moved offscreen canvas to backend

This commit is contained in:
Thomas Friedel 2019-02-22 12:34:08 +01:00
parent a0dfb6f530
commit 8dc91b34f5
12 changed files with 122 additions and 211 deletions

View file

@ -10,6 +10,8 @@ import (
// drawing. This enables the backend to be implemented by
// various methods (OpenGL, but also other APIs or software)
type Backend interface {
Size() (int, int)
LoadImage(img image.Image) (Image, error)
LoadLinearGradient(data *LinearGradientData) LinearGradient
LoadRadialGradient(data *RadialGradientData) RadialGradient

View file

@ -7,11 +7,15 @@ import (
)
func (b *GoGLBackend) ClearClip() {
b.activate()
gl.StencilMask(0xFF)
gl.Clear(gl.STENCIL_BUFFER_BIT)
}
func (b *GoGLBackend) Clip(pts [][2]float64) {
b.activate()
b.ptsBuf = b.ptsBuf[:0]
b.ptsBuf = append(b.ptsBuf,
0, 0,

View file

@ -10,6 +10,8 @@ import (
)
func (b *GoGLBackend) Clear(pts [4][2]float64) {
b.activate()
// first check if the four points are aligned to form a nice rectangle, which can be more easily
// cleared using glScissor and glClear
aligned := pts[0][0] == pts[1][0] && pts[2][0] == pts[3][0] && pts[0][1] == pts[3][1] && pts[1][1] == pts[2][1]
@ -68,6 +70,8 @@ func (b *GoGLBackend) clearRect(x, y, w, h int) {
}
func (b *GoGLBackend) Fill(style *backendbase.FillStyle, pts [][2]float64) {
b.activate()
if style.Blur > 0 {
b.offscr1.alpha = true
b.enableTextureRenderTarget(&b.offscr1)
@ -140,6 +144,8 @@ func (b *GoGLBackend) Fill(style *backendbase.FillStyle, pts [][2]float64) {
}
func (b *GoGLBackend) FillImageMask(style *backendbase.FillStyle, mask *image.Alpha, pts [][2]float64) {
b.activate()
w, h := mask.Rect.Dx(), mask.Rect.Dy()
gl.ActiveTexture(gl.TEXTURE1)

View file

@ -42,6 +42,10 @@ type GoGLBackend struct {
ptsBuf []float32
offscreen bool
offscrBuf offscreenBuffer
offscrImg Image
glChan chan func()
}
@ -54,7 +58,7 @@ type offscreenBuffer struct {
alpha bool
}
func New(x, y, w, h int) (backendbase.Backend, error) {
func New(x, y, w, h int) (*GoGLBackend, error) {
err := gl.Init()
if err != nil {
return nil, err
@ -210,15 +214,31 @@ func New(x, y, w, h int) (backendbase.Backend, error) {
return b, nil
}
func NewOffscreen(w, h int, alpha bool) (*GoGLBackend, error) {
b, err := New(0, 0, w, h)
if err != nil {
return nil, err
}
b.offscreen = true
b.offscrBuf.alpha = alpha
return b, 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) {
if !b.offscreen {
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)
}
func (b *GoGLBackend) Size() (int, int) {
return b.w, b.h
}
func glError() error {
glErr := gl.GetError()
if glErr != gl.NO_ERROR {
@ -227,21 +247,44 @@ func glError() error {
return nil
}
// Activate makes this GL backend active and sets the viewport. Only
// needs to be called if any other GL code changes the viewport
func (b *GoGLBackend) Activate() {
// if b.offscreen {
// gl.Viewport(0, 0, int32(cv.w), int32(cv.h))
// cv.enableTextureRenderTarget(&cv.offscrBuf)
// cv.offscrImg.w = cv.offscrBuf.w
// cv.offscrImg.h = cv.offscrBuf.h
// cv.offscrImg.tex = cv.offscrBuf.tex
// } else {
var activeContext *GoGLBackend
func (b *GoGLBackend) activate() {
if activeContext != b {
activeContext = b
if b.offscreen {
gl.Viewport(0, 0, int32(b.w), int32(b.h))
b.enableTextureRenderTarget(&b.offscrBuf)
b.offscrImg.w = b.offscrBuf.w
b.offscrImg.h = b.offscrBuf.h
b.offscrImg.tex = b.offscrBuf.tex
} else {
gl.Viewport(int32(b.x), int32(b.y), int32(b.w), int32(b.h))
b.disableTextureRenderTarget()
// }
// b.applyScissor()
gl.Clear(gl.STENCIL_BUFFER_BIT)
}
}
loop:
for {
select {
case f := <-b.glChan:
f()
default:
break loop
}
}
}
func (b *GoGLBackend) DeleteOffscreen() {
if !b.offscreen {
return
}
gl.DeleteTextures(1, &b.offscrBuf.tex)
gl.DeleteFramebuffers(1, &b.offscrBuf.frameBuf)
gl.DeleteRenderbuffers(1, &b.offscrBuf.renderStencilBuf)
b.offscreen = false
b.activate()
}
type glColor struct {
@ -439,11 +482,11 @@ func (b *GoGLBackend) enableTextureRenderTarget(offscr *offscreenBuffer) {
}
func (b *GoGLBackend) disableTextureRenderTarget() {
// if b.offscreen {
// b.enableTextureRenderTarget(&b.offscrBuf)
// } else {
if b.offscreen {
b.enableTextureRenderTarget(&b.offscrBuf)
} else {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
// }
}
}
type mat [9]float64

View file

@ -25,6 +25,7 @@ type RadialGradient struct {
}
type gradient struct {
b *GoGLBackend
from, to vec
tex uint32
loaded bool
@ -33,8 +34,10 @@ type gradient struct {
}
func (b *GoGLBackend) LoadLinearGradient(data *backendbase.LinearGradientData) backendbase.LinearGradient {
b.activate()
lg := &LinearGradient{
gradient: gradient{from: vec{data.X0, data.Y0}, to: vec{data.X1, data.Y1}, opaque: true},
gradient: gradient{b: b, from: vec{data.X0, data.Y0}, to: vec{data.X1, data.Y1}, opaque: true},
}
gl.GenTextures(1, &lg.tex)
gl.ActiveTexture(gl.TEXTURE0)
@ -53,8 +56,10 @@ func (b *GoGLBackend) LoadLinearGradient(data *backendbase.LinearGradientData) b
}
func (b *GoGLBackend) LoadRadialGradient(data *backendbase.RadialGradientData) backendbase.RadialGradient {
b.activate()
rg := &RadialGradient{
gradient: gradient{from: vec{data.X0, data.Y0}, to: vec{data.X1, data.Y1}, opaque: true},
gradient: gradient{b: b, from: vec{data.X0, data.Y0}, to: vec{data.X1, data.Y1}, opaque: true},
radFrom: data.RadFrom,
radTo: data.RadTo,
}
@ -76,6 +81,8 @@ func (b *GoGLBackend) LoadRadialGradient(data *backendbase.RadialGradientData) b
// Delete explicitly deletes the gradient
func (g *gradient) Delete() {
g.b.activate()
gl.DeleteTextures(1, &g.tex)
g.deleted = true
}
@ -91,6 +98,8 @@ func (g *gradient) load(stops backendbase.Gradient) {
return
}
g.b.activate()
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, g.tex)
var pixels [2048 * 4]byte

View file

@ -10,7 +10,7 @@ import (
// GetImageData returns an RGBA image of the current image
func (b *GoGLBackend) GetImageData(x, y, w, h int) *image.RGBA {
// cv.activate()
b.activate()
if x < 0 {
w += x
@ -44,7 +44,7 @@ func (b *GoGLBackend) GetImageData(x, y, w, h int) *image.RGBA {
// PutImageData puts the given image at the given x/y coordinates
func (b *GoGLBackend) PutImageData(img *image.RGBA, x, y int) {
// cv.activate()
b.activate()
gl.ActiveTexture(gl.TEXTURE0)
if b.imageBufTex == 0 {

View file

@ -12,6 +12,7 @@ import (
// Image represents a loaded image that can be used in various drawing functions
type Image struct {
b *GoGLBackend
w, h int
tex uint32
deleted bool
@ -19,6 +20,8 @@ type Image struct {
}
func (b *GoGLBackend) LoadImage(src image.Image) (backendbase.Image, error) {
b.activate()
var tex uint32
gl.GenTextures(1, &tex)
gl.ActiveTexture(gl.TEXTURE0)
@ -184,6 +187,8 @@ func (img *Image) Size() (int, int) { return img.w, img.h }
// Delete deletes the image from memory. Any draw calls
// with a deleted image will not do anything
func (img *Image) Delete() {
img.b.activate()
gl.DeleteTextures(1, &img.tex)
img.deleted = true
}
@ -194,6 +199,8 @@ func (img *Image) IsDeleted() bool { return img.deleted }
// Replace replaces the image with the new one
func (img *Image) Replace(src image.Image) error {
img.b.activate()
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, img.tex)
newImg, err := loadImage(src, img.tex)
@ -209,6 +216,8 @@ func (img *Image) Replace(src image.Image) error {
func (img *Image) IsOpaque() bool { return img.opaque }
func (b *GoGLBackend) DrawImage(dimg backendbase.Image, sx, sy, sw, sh, dx, dy, dw, dh float64, alpha float64) {
b.activate()
img := dimg.(*Image)
sx /= float64(img.w)

153
canvas.go
View file

@ -20,8 +20,8 @@ import (
type Canvas struct {
b backendbase.Backend
x, y, w, h int
fx, fy, fw, fh float64
w, h int
fw, fh float64
path Path2D
convex bool
@ -30,10 +30,6 @@ type Canvas struct {
state drawState
stateStack []drawState
offscreen bool
offscrBuf offscreenBuffer
offscrImg Image
images map[interface{}]*Image
shadowBuf [][2]float64
@ -136,16 +132,14 @@ var Performance = struct {
// While all functions on the canvas use the top left point as
// the origin, since GL uses the bottom left coordinate, the
// coordinates given here also use the bottom left as origin
func New(backend backendbase.Backend, x, y, w, h int) *Canvas {
if gli == nil {
panic("LoadGL must be called before a canvas can be created")
}
func New(backend backendbase.Backend) *Canvas {
cv := &Canvas{
b: backend,
stateStack: make([]drawState, 0, 20),
images: make(map[interface{}]*Image),
}
cv.SetBounds(x, y, w, h)
w, h := backend.Size()
cv.setBounds(w, h)
cv.state.lineWidth = 1
cv.state.lineAlpha = 1
cv.state.miterLimitSqr = 100
@ -156,36 +150,9 @@ func New(backend backendbase.Backend, x, y, w, h int) *Canvas {
return cv
}
// NewOffscreen creates a new canvas with the given size. It
// does not render directly to the screen but renders to a
// texture instead. If alpha is set to true, the offscreen
// canvas will have an alpha channel
func NewOffscreen(backend backendbase.Backend, w, h int, alpha bool) *Canvas {
cv := New(backend, 0, 0, w, h)
cv.offscreen = true
cv.offscrBuf.alpha = alpha
return cv
}
func DeleteOffscreen(cv *Canvas) {
if !cv.offscreen {
return
}
gli.DeleteTextures(1, &cv.offscrBuf.tex)
gli.DeleteFramebuffers(1, &cv.offscrBuf.frameBuf)
gli.DeleteRenderbuffers(1, &cv.offscrBuf.renderStencilBuf)
}
// SetBounds updates the bounds of the canvas. This would
// usually be called for example when the window is resized
func (cv *Canvas) SetBounds(x, y, w, h int) {
if !cv.offscreen {
cv.x, cv.y = x, y
cv.fx, cv.fy = float64(x), float64(y)
}
func (cv *Canvas) setBounds(w, h int) {
cv.w, cv.h = w, h
cv.fw, cv.fh = float64(w), float64(h)
activeCanvas = nil
}
// Width returns the internal width of the canvas
@ -202,46 +169,8 @@ func (cv *Canvas) tf(v vec) vec {
return v
}
// Activate makes the canvas active and sets the viewport. Only needs
// to be called if any other GL code changes the viewport
func (cv *Canvas) Activate() {
if cv.offscreen {
gli.Viewport(0, 0, int32(cv.w), int32(cv.h))
cv.enableTextureRenderTarget(&cv.offscrBuf)
// cv.offscrImg.w = cv.offscrBuf.w
// cv.offscrImg.h = cv.offscrBuf.h
// cv.offscrImg.tex = cv.offscrBuf.tex
} else {
gli.Viewport(int32(cv.x), int32(cv.y), int32(cv.w), int32(cv.h))
cv.disableTextureRenderTarget()
}
}
var activeCanvas *Canvas
func (cv *Canvas) activate() {
if activeCanvas != cv {
activeCanvas = cv
cv.Activate()
}
loop:
for {
select {
case f := <-glChan:
f()
default:
break loop
}
}
}
const alphaTexSize = 2048
var (
gli GL
glChan = make(chan func())
)
type offscreenBuffer struct {
tex uint32
w int
@ -261,23 +190,6 @@ type gaussianShader struct {
kernel int32
}
// LoadGL needs to be called once per GL context to load the GL assets
// that canvas needs. The parameter is an implementation of the GL interface
// in this package that should make this package neutral to GL implementations.
// The goglimpl subpackage contains an implementation based on Go-GL v3.2
func LoadGL(glimpl GL) (err error) {
gli = glimpl
return
}
func glError() error {
glErr := gli.GetError()
if glErr != gl_NO_ERROR {
return fmt.Errorf("GL Error: %x", glErr)
}
return nil
}
// SetFillStyle sets the color, gradient, or image for any fill calls. To set a
// color, there are several acceptable formats: 3 or 4 int values for RGB(A) in
// the range 0-255, 3 or 4 float values for RGB(A) in the range 0-1, hex strings
@ -349,57 +261,6 @@ func (cv *Canvas) backendFillStyle(s *drawStyle, alpha float64) backendbase.Fill
return stl
}
func (cv *Canvas) enableTextureRenderTarget(offscr *offscreenBuffer) {
if offscr.w != cv.w || offscr.h != cv.h {
if offscr.w != 0 && offscr.h != 0 {
gli.DeleteTextures(1, &offscr.tex)
gli.DeleteFramebuffers(1, &offscr.frameBuf)
gli.DeleteRenderbuffers(1, &offscr.renderStencilBuf)
}
offscr.w = cv.w
offscr.h = cv.h
gli.ActiveTexture(gl_TEXTURE0)
gli.GenTextures(1, &offscr.tex)
gli.BindTexture(gl_TEXTURE_2D, offscr.tex)
// todo do non-power-of-two textures work everywhere?
if offscr.alpha {
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGBA, int32(cv.w), int32(cv.h), 0, gl_RGBA, gl_UNSIGNED_BYTE, nil)
} else {
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGB, int32(cv.w), int32(cv.h), 0, gl_RGB, gl_UNSIGNED_BYTE, nil)
}
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_NEAREST)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_NEAREST)
gli.GenFramebuffers(1, &offscr.frameBuf)
gli.BindFramebuffer(gl_FRAMEBUFFER, offscr.frameBuf)
gli.GenRenderbuffers(1, &offscr.renderStencilBuf)
gli.BindRenderbuffer(gl_RENDERBUFFER, offscr.renderStencilBuf)
gli.RenderbufferStorage(gl_RENDERBUFFER, gl_DEPTH24_STENCIL8, int32(cv.w), int32(cv.h))
gli.FramebufferRenderbuffer(gl_FRAMEBUFFER, gl_DEPTH_STENCIL_ATTACHMENT, gl_RENDERBUFFER, offscr.renderStencilBuf)
gli.FramebufferTexture(gl_FRAMEBUFFER, gl_COLOR_ATTACHMENT0, offscr.tex, 0)
if err := gli.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))
}
gli.Clear(gl_COLOR_BUFFER_BIT | gl_STENCIL_BUFFER_BIT)
} else {
gli.BindFramebuffer(gl_FRAMEBUFFER, offscr.frameBuf)
}
}
func (cv *Canvas) disableTextureRenderTarget() {
if cv.offscreen {
cv.enableTextureRenderTarget(&cv.offscrBuf)
} else {
gli.BindFramebuffer(gl_FRAMEBUFFER, 0)
}
}
// SetLineWidth sets the line width for any line drawing calls
func (cv *Canvas) SetLineWidth(width float64) {
if width < 0 {
@ -430,7 +291,7 @@ func (cv *Canvas) SetFont(src interface{}, size float64) {
if f, ok := fonts[v]; ok {
cv.state.font = f
} else {
f, err := LoadFont(v)
f, err := cv.LoadFont(v)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading font %s: %v\n", v, err)
fonts[v] = nil

View file

@ -124,13 +124,13 @@ func (img *Image) Replace(src interface{}) error {
// source coordinates
func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
var img *Image
var flip bool
if cv2, ok := image.(*Canvas); ok && cv2.offscreen {
img = &cv2.offscrImg
flip = true
} else {
// var flip bool
// if cv2, ok := image.(*Canvas); ok && cv2.offscreen {
// img = &cv2.offscrImg
// flip = true
// } else {
img = cv.getImage(image)
}
// }
if img == nil {
return
@ -140,8 +140,6 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
return
}
cv.activate()
var sx, sy, sw, sh, dx, dy, dw, dh float64
sw, sh = float64(img.Width()), float64(img.Height())
dw, dh = float64(img.Width()), float64(img.Height())
@ -157,10 +155,10 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
dw, dh = coords[6], coords[7]
}
if flip {
dy += dh
dh = -dh
}
// if flip {
// dy += dh
// dh = -dh
// }
var data [4][2]float64
data[0] = cv.tf(vec{dx, dy})

View file

@ -96,8 +96,6 @@ func (cv *Canvas) strokePath(path *Path2D) {
return
}
cv.activate()
dashedPath := cv.applyLineDash(path.p)
var triBuf [500][2]float64
@ -328,7 +326,6 @@ func (cv *Canvas) FillPath(path *Path2D) {
if len(path.p) < 3 {
return
}
cv.activate()
var triBuf [500][2]float64
tris := buildFillTriangles(path, triBuf[:0])
@ -431,8 +428,6 @@ func (cv *Canvas) StrokeRect(x, y, w, h float64) {
// FillRect fills a rectangle with the active fill style
func (cv *Canvas) FillRect(x, y, w, h float64) {
cv.activate()
p0 := cv.tf(vec{x, y})
p1 := cv.tf(vec{x, y + h})
p2 := cv.tf(vec{x + w, y + h})
@ -448,8 +443,6 @@ func (cv *Canvas) FillRect(x, y, w, h float64) {
// ClearRect sets the color of the rectangle to transparent black
func (cv *Canvas) ClearRect(x, y, w, h float64) {
cv.activate()
p0 := cv.tf(vec{x, y})
p1 := cv.tf(vec{x, y + h})
p2 := cv.tf(vec{x + w, y + h})

View file

@ -12,9 +12,7 @@ import (
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/tfriedel6/canvas"
"github.com/tfriedel6/canvas/backend/backendbase"
"github.com/tfriedel6/canvas/backend/gogl"
"github.com/tfriedel6/canvas/glimpl/gogl"
"github.com/veandco/go-sdl2/sdl"
)
@ -24,7 +22,7 @@ type Window struct {
Window *sdl.Window
WindowID uint32
GLContext sdl.GLContext
Backend backendbase.Backend
Backend *goglbackend.GoGLBackend
canvas *canvas.Canvas
frameTimes [10]time.Time
frameIndex int
@ -98,12 +96,7 @@ func CreateWindow(w, h int, title string) (*Window, *canvas.Canvas, error) {
sdl.GLSetSwapInterval(1)
gl.Enable(gl.MULTISAMPLE)
err = canvas.LoadGL(glimplgogl.GLImpl{})
if err != nil {
return nil, nil, fmt.Errorf("Error loading canvas GL assets: %v", err)
}
cv := canvas.New(backend, 0, 0, w, h)
cv := canvas.New(backend)
wnd := &Window{
Window: window,
WindowID: windowID,
@ -195,7 +188,7 @@ func (wnd *Window) StartFrame() error {
wnd.SizeChange(int(e.Data1), int(e.Data2))
handled = true
} else {
wnd.canvas.SetBounds(0, 0, int(e.Data1), int(e.Data2))
wnd.Backend.SetBounds(0, 0, int(e.Data1), int(e.Data2))
}
}
}

View file

@ -28,10 +28,7 @@ var defaultFont *Font
// LoadFont loads a font and returns the result. The font
// can be a file name or a byte slice in TTF format
func LoadFont(src interface{}) (*Font, error) {
if gli == nil {
panic("LoadGL must be called before fonts can be loaded")
}
func (cv *Canvas) LoadFont(src interface{}) (*Font, error) {
var f *Font
switch v := src.(type) {
case *truetype.Font:
@ -64,8 +61,6 @@ func LoadFont(src interface{}) (*Font, error) {
// FillText draws the given string at the given coordinates
// using the currently set font and font height
func (cv *Canvas) FillText(str string, x, y float64) {
cv.activate()
if cv.state.font == nil {
return
}
@ -227,8 +222,6 @@ func (cv *Canvas) FillText(str string, x, y float64) {
// using the currently set font and font height and using the
// current stroke style
func (cv *Canvas) StrokeText(str string, x, y float64) {
cv.activate()
if cv.state.font == nil {
return
}