canvas/backend/gogl/gogl.go
2019-03-28 11:25:59 +01:00

585 lines
16 KiB
Go

package goglbackend
import (
"fmt"
"image/color"
"math"
"github.com/tfriedel6/canvas/backend/backendbase"
"github.com/tfriedel6/canvas/backend/gogl/gl"
)
const alphaTexSize = 2048
var zeroes [alphaTexSize]byte
// GLContext is a context that contains all the
// shaders and buffers necessary for rendering
type GLContext struct {
buf uint32
shadowBuf uint32
alphaTex uint32
sr solidShader
lgr linearGradientShader
rgr radialGradientShader
ipr imagePatternShader
sar solidAlphaShader
rgar radialGradientAlphaShader
lgar linearGradientAlphaShader
ipar imagePatternAlphaShader
ir imageShader
gauss15r gaussianShader
gauss63r gaussianShader
gauss127r gaussianShader
offscr1 offscreenBuffer
offscr2 offscreenBuffer
imageBufTex uint32
imageBuf []byte
ptsBuf []float32
glChan chan func()
}
// NewGLContext creates all the necessary GL resources,
// like shaders and buffers
func NewGLContext() (*GLContext, error) {
ctx := &GLContext{
ptsBuf: make([]float32, 0, 4096),
glChan: make(chan func()),
}
err := gl.Init()
if err != nil {
return nil, err
}
gl.GetError() // clear error state
err = loadShader(solidVS, solidFS, &ctx.sr.shaderProgram)
if err != nil {
return nil, err
}
ctx.sr.shaderProgram.mustLoadLocations(&ctx.sr)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(linearGradientVS, linearGradientFS, &ctx.lgr.shaderProgram)
if err != nil {
return nil, err
}
ctx.lgr.shaderProgram.mustLoadLocations(&ctx.lgr)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(radialGradientVS, radialGradientFS, &ctx.rgr.shaderProgram)
if err != nil {
return nil, err
}
ctx.rgr.shaderProgram.mustLoadLocations(&ctx.rgr)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(imagePatternVS, imagePatternFS, &ctx.ipr.shaderProgram)
if err != nil {
return nil, err
}
ctx.ipr.shaderProgram.mustLoadLocations(&ctx.ipr)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(solidAlphaVS, solidAlphaFS, &ctx.sar.shaderProgram)
if err != nil {
return nil, err
}
ctx.sar.shaderProgram.mustLoadLocations(&ctx.sar)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(linearGradientAlphaVS, linearGradientFS, &ctx.lgar.shaderProgram)
if err != nil {
return nil, err
}
ctx.lgar.shaderProgram.mustLoadLocations(&ctx.lgar)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(radialGradientAlphaVS, radialGradientAlphaFS, &ctx.rgar.shaderProgram)
if err != nil {
return nil, err
}
ctx.rgar.shaderProgram.mustLoadLocations(&ctx.rgar)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(imagePatternAlphaVS, imagePatternAlphaFS, &ctx.ipar.shaderProgram)
if err != nil {
return nil, err
}
ctx.ipar.shaderProgram.mustLoadLocations(&ctx.ipar)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(imageVS, imageFS, &ctx.ir.shaderProgram)
if err != nil {
return nil, err
}
ctx.ir.shaderProgram.mustLoadLocations(&ctx.ir)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(gaussian15VS, gaussian15FS, &ctx.gauss15r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss15r.shaderProgram.mustLoadLocations(&ctx.gauss15r)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(gaussian63VS, gaussian63FS, &ctx.gauss63r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss63r.shaderProgram.mustLoadLocations(&ctx.gauss63r)
if err = glError(); err != nil {
return nil, err
}
err = loadShader(gaussian127VS, gaussian127FS, &ctx.gauss127r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss127r.shaderProgram.mustLoadLocations(&ctx.gauss127r)
if err = glError(); err != nil {
return nil, err
}
gl.GenBuffers(1, &ctx.buf)
if err = glError(); err != nil {
return nil, err
}
gl.GenBuffers(1, &ctx.shadowBuf)
if err = glError(); err != nil {
return nil, err
}
gl.ActiveTexture(gl.TEXTURE0)
gl.GenTextures(1, &ctx.alphaTex)
gl.BindTexture(gl.TEXTURE_2D, ctx.alphaTex)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, alphaTexSize, alphaTexSize, 0, gl.ALPHA, gl.UNSIGNED_BYTE, nil)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Enable(gl.STENCIL_TEST)
gl.StencilMask(0xFF)
gl.Clear(gl.STENCIL_BUFFER_BIT)
gl.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP)
gl.StencilFunc(gl.EQUAL, 0, 0xFF)
gl.Disable(gl.SCISSOR_TEST)
return ctx, nil
}
// GoGLBackend is a canvas backend using Go-GL
type GoGLBackend struct {
x, y, w, h int
fx, fy, fw, fh float64
*GLContext
curClip [][2]float64
activateFn func()
disableTextureRenderTarget func()
}
type offscreenBuffer struct {
tex uint32
w int
h int
renderStencilBuf uint32
frameBuf uint32
alpha bool
}
// New returns a new canvas backend. x, y, w, h define the target
// rectangle in the window. ctx is a GLContext created with
// NewGLContext, but can be nil for a default one. It makes sense
// to pass one in when using for example an onscreen and an
// offscreen backend using the same GL context.
func New(x, y, w, h int, ctx *GLContext) (*GoGLBackend, error) {
if ctx == nil {
var err error
ctx, err = NewGLContext()
if err != nil {
return nil, err
}
}
b := &GoGLBackend{
w: w,
h: h,
fw: float64(w),
fh: float64(h),
GLContext: ctx,
}
b.activateFn = func() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.Viewport(int32(b.x), int32(b.y), int32(b.w), int32(b.h))
gl.Clear(gl.STENCIL_BUFFER_BIT)
if c := b.curClip; c != nil {
b.curClip = nil
b.Clip(c)
}
}
b.disableTextureRenderTarget = func() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
}
return b, nil
}
// GoGLBackendOffscreen is a canvas backend using an offscreen
// texture
type GoGLBackendOffscreen struct {
GoGLBackend
TextureID uint32
offscrBuf offscreenBuffer
offscrImg Image
}
// NewOffscreen returns a new offscreen canvas backend. w, h define
// the size of the offscreen texture. ctx is a GLContext created
// with NewGLContext, but can be nil for a default one. It makes
// sense to pass one in when using for example an onscreen and an
// offscreen backend using the same GL context.
func NewOffscreen(w, h int, alpha bool, ctx *GLContext) (*GoGLBackendOffscreen, error) {
b, err := New(0, 0, w, h, ctx)
if err != nil {
return nil, err
}
bo := &GoGLBackendOffscreen{}
bo.offscrBuf.alpha = alpha
bo.offscrImg.flip = true
b.activateFn = func() {
b.enableTextureRenderTarget(&bo.offscrBuf)
gl.Viewport(0, 0, int32(bo.GoGLBackend.w), int32(bo.GoGLBackend.h))
bo.offscrImg.w = bo.offscrBuf.w
bo.offscrImg.h = bo.offscrBuf.h
bo.offscrImg.tex = bo.offscrBuf.tex
bo.TextureID = bo.offscrBuf.tex
}
b.disableTextureRenderTarget = func() {
b.enableTextureRenderTarget(&bo.offscrBuf)
}
bo.GoGLBackend = *b
return bo, nil
}
// SetBounds updates the bounds of the canvas. This would
// usually be called for example when the window is resized
func (b *GoGLBackend) SetBounds(x, y, w, h int) {
b.x, b.y = x, y
b.fx, b.fy = float64(x), float64(y)
b.w, b.h = w, h
b.fw, b.fh = float64(w), float64(h)
if b == activeContext {
gl.Viewport(0, 0, int32(b.w), int32(b.h))
gl.Clear(gl.STENCIL_BUFFER_BIT)
}
}
// SetSize updates the size of the offscreen texture
func (b *GoGLBackendOffscreen) SetSize(w, h int) {
b.GoGLBackend.SetBounds(0, 0, w, h)
b.enableTextureRenderTarget(&b.offscrBuf)
b.offscrImg.w = b.offscrBuf.w
b.offscrImg.h = b.offscrBuf.h
}
// Size returns the size of the window or offscreen
// texture
func (b *GoGLBackend) Size() (int, int) {
return b.w, b.h
}
func glError() error {
glErr := gl.GetError()
if glErr != gl.NO_ERROR {
return fmt.Errorf("GL Error: %x", glErr)
}
return nil
}
// Activate only needs to be called if there is other
// code also using the GL state
func (b *GoGLBackend) Activate() {
activeContext = nil
b.activate()
}
var activeContext *GoGLBackend
func (b *GoGLBackend) activate() {
if activeContext != b {
activeContext = b
b.activateFn()
}
b.runGLQueue()
}
func (b *GoGLBackend) runGLQueue() {
for {
select {
case f := <-b.glChan:
f()
default:
return
}
}
}
// Delete deletes the offscreen texture. After calling this
// the backend can no longer be used
func (b *GoGLBackendOffscreen) Delete() {
gl.DeleteTextures(1, &b.offscrBuf.tex)
gl.DeleteFramebuffers(1, &b.offscrBuf.frameBuf)
gl.DeleteRenderbuffers(1, &b.offscrBuf.renderStencilBuf)
}
// CanUseAsImage returns true if the given backend can be
// directly used by this backend to avoid a conversion.
// Used internally
func (b *GoGLBackend) CanUseAsImage(b2 backendbase.Backend) bool {
_, ok := b2.(*GoGLBackendOffscreen)
return ok
}
// AsImage returns nil, since this backend cannot be directly
// used as an image. Used internally
func (b *GoGLBackend) AsImage() backendbase.Image {
return nil
}
// AsImage returns an implementation of the Image interface
// that can be used to render this offscreen texture
// directly. Used internally
func (b *GoGLBackendOffscreen) AsImage() backendbase.Image {
return &b.offscrImg
}
type glColor struct {
r, g, b, a float64
}
func colorGoToGL(c color.RGBA) glColor {
var glc glColor
glc.r = float64(c.R) / 255
glc.g = float64(c.G) / 255
glc.b = float64(c.B) / 255
glc.a = float64(c.A) / 255
return glc
}
func (b *GoGLBackend) useShader(style *backendbase.FillStyle) (vertexLoc uint32) {
if lg := style.LinearGradient; lg != nil {
lg := lg.(*LinearGradient)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, lg.tex)
gl.UseProgram(b.lgr.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
dir := to.sub(from)
length := dir.len()
dir = dir.scale(1 / length)
gl.Uniform2f(b.lgr.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.lgr.From, float32(from[0]), float32(from[1]))
gl.Uniform2f(b.lgr.Dir, float32(dir[0]), float32(dir[1]))
gl.Uniform1f(b.lgr.Len, float32(length))
gl.Uniform1i(b.lgr.Gradient, 0)
gl.Uniform1f(b.lgr.GlobalAlpha, float32(style.Color.A)/255)
return b.lgr.Vertex
}
if rg := style.RadialGradient; rg != nil {
rg := rg.(*RadialGradient)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, rg.tex)
gl.UseProgram(b.rgr.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
gl.Uniform2f(b.rgr.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.rgr.From, float32(from[0]), float32(from[1]))
gl.Uniform2f(b.rgr.To, float32(to[0]), float32(to[1]))
gl.Uniform1f(b.rgr.RadFrom, float32(style.Gradient.RadFrom))
gl.Uniform1f(b.rgr.RadTo, float32(style.Gradient.RadTo))
gl.Uniform1i(b.rgr.Gradient, 0)
gl.Uniform1f(b.rgr.GlobalAlpha, float32(style.Color.A)/255)
return b.rgr.Vertex
}
if ip := style.ImagePattern; ip != nil {
img := ip.(*ImagePattern).data.Image.(*Image)
gl.UseProgram(b.ipr.ID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, img.tex)
gl.Uniform2f(b.ipr.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.ipr.ImageSize, float32(img.w), float32(img.h))
gl.Uniform1i(b.ipr.Image, 0)
gl.Uniform1f(b.ipr.GlobalAlpha, float32(style.Color.A)/255)
return b.ipr.Vertex
}
gl.UseProgram(b.sr.ID)
gl.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh))
c := colorGoToGL(style.Color)
gl.Uniform4f(b.sr.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
gl.Uniform1f(b.sr.GlobalAlpha, 1)
return b.sr.Vertex
}
func (b *GoGLBackend) useAlphaShader(style *backendbase.FillStyle, alphaTexSlot int32) (vertexLoc, alphaTexCoordLoc uint32) {
if lg := style.LinearGradient; lg != nil {
lg := lg.(*LinearGradient)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, lg.tex)
gl.UseProgram(b.lgar.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
dir := to.sub(from)
length := dir.len()
dir = dir.scale(1 / length)
gl.Uniform2f(b.lgar.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.lgar.From, float32(from[0]), float32(from[1]))
gl.Uniform2f(b.lgar.Dir, float32(dir[0]), float32(dir[1]))
gl.Uniform1f(b.lgar.Len, float32(length))
gl.Uniform1i(b.lgar.Gradient, 0)
gl.Uniform1i(b.lgar.AlphaTex, alphaTexSlot)
gl.Uniform1f(b.lgar.GlobalAlpha, float32(style.Color.A)/255)
return b.lgar.Vertex, b.lgar.AlphaTexCoord
}
if rg := style.RadialGradient; rg != nil {
rg := rg.(*RadialGradient)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, rg.tex)
gl.UseProgram(b.rgar.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
gl.Uniform2f(b.rgar.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.rgar.From, float32(from[0]), float32(from[1]))
gl.Uniform2f(b.rgar.To, float32(to[0]), float32(to[1]))
gl.Uniform1f(b.rgar.RadFrom, float32(style.Gradient.RadFrom))
gl.Uniform1f(b.rgar.RadTo, float32(style.Gradient.RadTo))
gl.Uniform1i(b.rgar.Gradient, 0)
gl.Uniform1i(b.rgar.AlphaTex, alphaTexSlot)
gl.Uniform1f(b.rgar.GlobalAlpha, float32(style.Color.A)/255)
return b.rgar.Vertex, b.rgar.AlphaTexCoord
}
if ip := style.ImagePattern; ip != nil {
img := ip.(*ImagePattern).data.Image.(*Image)
gl.UseProgram(b.ipar.ID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, img.tex)
gl.Uniform2f(b.ipar.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform2f(b.ipar.ImageSize, float32(img.w), float32(img.h))
gl.Uniform1i(b.ipar.Image, 0)
gl.Uniform1i(b.ipar.AlphaTex, alphaTexSlot)
gl.Uniform1f(b.ipar.GlobalAlpha, float32(style.Color.A)/255)
return b.ipar.Vertex, b.ipar.AlphaTexCoord
}
gl.UseProgram(b.sar.ID)
gl.Uniform2f(b.sar.CanvasSize, float32(b.fw), float32(b.fh))
c := colorGoToGL(style.Color)
gl.Uniform4f(b.sar.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
gl.Uniform1i(b.sar.AlphaTex, alphaTexSlot)
gl.Uniform1f(b.sar.GlobalAlpha, 1)
return b.sar.Vertex, b.sar.AlphaTexCoord
}
func (b *GoGLBackend) enableTextureRenderTarget(offscr *offscreenBuffer) {
if offscr.w == b.w && offscr.h == b.h {
gl.BindFramebuffer(gl.FRAMEBUFFER, offscr.frameBuf)
return
}
if b.w == 0 || b.h == 0 {
return
}
if offscr.w != 0 && offscr.h != 0 {
gl.DeleteTextures(1, &offscr.tex)
gl.DeleteFramebuffers(1, &offscr.frameBuf)
gl.DeleteRenderbuffers(1, &offscr.renderStencilBuf)
}
offscr.w = b.w
offscr.h = b.h
gl.ActiveTexture(gl.TEXTURE0)
gl.GenTextures(1, &offscr.tex)
gl.BindTexture(gl.TEXTURE_2D, offscr.tex)
// todo do non-power-of-two textures work everywhere?
if offscr.alpha {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(b.w), int32(b.h), 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
} else {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, int32(b.w), int32(b.h), 0, gl.RGB, gl.UNSIGNED_BYTE, nil)
}
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.GenFramebuffers(1, &offscr.frameBuf)
gl.BindFramebuffer(gl.FRAMEBUFFER, offscr.frameBuf)
gl.GenRenderbuffers(1, &offscr.renderStencilBuf)
gl.BindRenderbuffer(gl.RENDERBUFFER, offscr.renderStencilBuf)
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, int32(b.w), int32(b.h))
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, offscr.renderStencilBuf)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, offscr.tex, 0)
if err := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); err != gl.FRAMEBUFFER_COMPLETE {
// todo this should maybe not panic
panic(fmt.Sprintf("Failed to set up framebuffer for offscreen texture: %x", err))
}
gl.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
}
type vec [2]float64
func (v vec) sub(v2 vec) vec {
return vec{v[0] - v2[0], v[1] - v2[1]}
}
func (v vec) len() float64 {
return math.Sqrt(v[0]*v[0] + v[1]*v[1])
}
func (v vec) scale(f float64) vec {
return vec{v[0] * f, v[1] * f}
}