grog

package module
v0.0.0-...-24e800f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2019 License: ISC Imports: 11 Imported by: 0

README

grog

grog is a fairly low-level 2D library for Go on top of OpenGL 2.1+ or OpenGLES 2.0+.

Features

The idea is to alleviate some of the pain of using the OpenGL API while using as few abstractions as possible and still providing full access to the OpenGL API.

  • An asset manager to handle asynchronous loading textures and fonts

  • Batch drawing of textures and regions. The main loop looks like:

        // keep track of screen or framebuffer geometry
        var fb grog.Screen
        // keep frame buffer size updated in the appropriate event handler
        // and gl.Viewport set accordingly.
        sw, sh := window.GetFramebufferSize()
        fb.SetSize(image.Pt(sw, sh))
        gl.Viewport(0, 0, sw, sh)
    
        // create a new concurrent batch
        b := grog.NewBatch(true)
    
        for !window.ShouldClose() {
            b.Begin()
    
            b.Camera(fb.View())
            b.Clear(color.RGBA{R:0,G:0,B:0,A:255})
    
            // sprites is defined somewhere else as var sprites []texture.Region
            for i := range sprites {
                b.Draw(&sprites[i], spritePos[i], grog.Pt(1, 1), 0, nil)
            }
            b.End()
    
            glfw.SwapBuffers()
            glfw.PollEvents()
        }
    
  • Concurrent and non-concurrent batch.

  • Text rendering (with very decent results).

  • Support for multiple independent views with out of the box support for zooming/panning.

  • grog is NOT tied into any OpenGL context creation toolkit like GLFW or SDL2. Use the one you like, roll your own event loop. See cmd/demo/demo_glfw.go.

Not really features, but worth mentioning:

  • NOT game oriented: since you have control over the event loop, feel free to wait for events before drawing. Or draw only whenever is necessary.
  • The loop sub-package provides implementations for different event loop models with configurable timestep and frame rate clamping.
  • No built-in Z coordinate handling. Z-order must be managed by the client code (just draw in the proper order). This might end-up being implemented, depending on available time for trying different solutions.
  • All OpenGL calls must be done from the main thread (this is required on some OSes).

Demo app

Run the demo:

go run ./cmd/demo

This will use the OpenGL 2.1 API with GLFW for window creation and OpenGL context handling. You can try with GLES2 API:

go run -tags "gles2" ./cmd/demo

Left mouse button + mouse or the arrow keys to pan the top view, mouse wheel to zoom-in/out and escape to quit. Press space to switch to a tilemap view with 320x 320 tiles of 16x16 pixels (that's 102400 tiles).

The "ups" value in the top right corner of the screen is 1/(average_render_time). This value can be misleading: you can have 60 fps and 120 ups but with the CPU at only 10% load and its clock speed well below its maximum. So 120 ups here doesn't mean that you can draw twice as many quads, it's in fact much more. The actual limit is when the ups value gets very close to the fps value.

On Linux, more precisely Ubuntu 18.04, there are a few animation hiccups when NOT running in fullscreen mode. This is the same for all OpenGL applications. (I suspect the compositor to silently drop frames). Just run in fullscreen if you need smooth animations.

Rationale

Before trying out grog, you might want to check out pixel, ebiten or engo for games, or fyne for UI apps. These engines are much more feature rich than grog.

So why another engine? The existing ones have either an API I don't like, don't expose OpenGL, are way too heavy for my needs, have tons of external dependencies (licensing nightmare), or a combination of those.

grog's API semantics are very close to OpenGL's (for better or worse) and mix well with custom OpenGL code.

grog's external dependencies are limited to:

  • golang.org/x/...
  • github.com/golang/freetype
  • some of my own repositories (which are and will remain under the same license as grog).

Additionally, you will need github.com/go-gl/glfw, github.com/veandco/go-sdl2/sdl or any other toolkit capable of creating GL contexts.

Of OpenGL bindings, cgo and performance

The OpenGL API bindings require cgo and performance of cgo calls is notoriously bad. This has however improved a lot and is currently around 80ns per call on a low end CPU with Go 1.12.

This is optimized in grog by using a batch (nothing new here), available in single threaded and concurrent versions plus custom OpenGL bindings (generated by gogl, NOT go-gl) that allow writing part of the GL code in C; like placing the usual call sequence glBindTexture, glBufferSubData and glDrawElements into a single C function; you get 3 cgo calls for the price of one.

This last optimization is not yet enabled, and if this proves to give only minor performance gains, we might end up using go-gl and/or android-go for the OpenGL bindings.

In its current state, grog can display over 70000 fully animated quads (including text rendering) at 60 fps on a low-end CPU (AMD FX6300) with the concurrent batch, and 50000 with the non-concurrent version. For reference, a tile map of 16x16 tiles on a 1920x1080 screen needs 8100 quads.

The concurrent batch does the model matrix transforms concurrently. This works well as long as quads are drawn from texture atlases. If gl.BindTexture needs to be called for every quad drawn (i.e. the batch is flushed after every quad), the 60 fps limit is reached at 5200 quads for the non-concurrent batch and only 640 for the concurrent one. On a i5 6300U @2.3GHz, this same test runs at 30 fps and 110 fps respectively. Channels are twice as fast on the i5 compared to the FX and this clearly shows that channels are the bottleneck in this scenario.

Speaking of Go channels, there's also gomobile/gl where OpenGL calls go through a worker goroutine via a channel. It has the interesting property that code calling OpenGL functions does not need to run on the main thread. However, considering that OpenGL is a state machine, state changes must be issued in a specific order in order to obtain a specific result. As a consequence, the rendering code must either use some complex sync mechanism between goroutines that draw things, or do everything from a single goroutine (back to square one). There are so many ways around this main-thread limitation that I don't really see any real benefit here. Additionally, using a channel results in a 450ns overhead per call on the same low-end CPU (half that on a i5 6300U @2.3GHz). This doesn't bode well performance wise, but I might test it at some point.

Supported platforms

Desktop: Anywhere you can create an use an OpenGL context with the OpenGL 2.1 API. This should cover Windows, macOS, Linux and BSDs. The gl sub-package may not provide the proper build flags for Windows/macOS; contributions welcome!

Mobile: Android support is planned. Contributions welcome for iOS.

Only OpenGL and OpenGL ES will be supported for the time being.

TODO

Missing features

In no particular order:

  • batch: add optional culling
  • batch: add a raw draw function where the caller has already done the model transform. Or some other way to easily implement relative positioning.
  • Add optional support for OpenGLES 3.x and higher versions of OpenGL (right now, OpenGLES 2.0 and OpenGL 2.1 only) => this depends on gogl
  • rotated text rendering
Tweaks
  • text: faster glyph cache map.
  • text: Implement a custom rasterizer based on golang.org/x/image/vector?
  • text: add hints/tips to package, like "for readable text, don't draw fonts at non-integer x/y coordinates"
  • batch: reduce allocs/GC usage.

grog is not a full fledged engine (yet)

By a full fledged engine, I mean an abstraction layer on top of SDL2 or GLFW (plus event loop). Both have very different event handling mechanisms: one uses an event queue while the other uses callbacks. The engine needs to somehow merge both into a single coherent API.

There's been some work on this topic (see the app package in the engine branch) but it quickly ran into some major issues, especially around input handling.

In a properly behaved game where the hero is moved with the WASD keys, because users with non QWERTY keyboards should not have to configure keyboard mappings before playing, all we care about in the event loop is the physical position (scancode) of the keys, regardless of the user's keyboard layout (mapping of scancodes to keycodes).

However, in the keyboard configuration UI, we want to display the keycode. So depending on the keyboard layout, we'll display something like "Move up -> W" or "Move Up -> Z". Same in some tutorial text: "Press Y to drop your weapon", or "Press Z..." for a German keyboard.

This is really easy to get right with SDL2 where scancodes are a well defined enumeration (regardless of OS), not so much with GLFW. Merging both behaviors into a single coherent API is no small task. Also this requires GLFW 3.3 for which Go bindings are not yet in a usable state: see go-gl/glfw#219, and go-gl/glfw#234 that needs to be backported. So I ended up disabling GLFW support until that PR landed, and besides some dumb init code for GL with SDL, the end result was a useless 1:1 layer on top of the SDL API.

Since the whole thing got pointless, for the time being, the main focus is on the core library. The demo is slowly being rewritten to decouple the backend from the demo code so that you can just grab the backend init code to get quickly up and running with your own application.

Documentation

Index

Constants

View Source
const (
	// see subPixels() in https://github.com/golang/freetype/blob/master/truetype/face.go
	FontSubPixelsX = 8

	FontSubPixelsY = 1
)
View Source
const (
	HintingNone     Hinting = Hinting(font.HintingNone)
	HintingVertical         = Hinting(font.HintingVertical)
	HintingFull             = Hinting(font.HintingFull)
)
View Source
const (
	Repeat         TextureWrap = gl.GL_REPEAT
	MirroredRepeat             = gl.GL_MIRRORED_REPEAT
	ClampToEdge                = gl.GL_CLAMP_TO_EDGE
)

WrapMode values map directly to their OpenGL equivalents.

Variables

View Source
var FontTextureSize int = 1024

Texture size for font glyph texture atlas. This value should be adjusted to be no larger than gl.GL_MAX_TEXTURE_SIZE:

var mts int32
gl.GetIntegerv(gl.GL_MAX_TEXTURE_SIZE, &mts)
if mts > int(TextureSize) || mts == 0 {
	TextureSize = int(mts)
}

Functions

This section is empty.

Types

type BatchRenderer

type BatchRenderer interface {
	Renderer
	Begin()
	Flush()
	End()
	Close()
}

func NewBatch

func NewBatch(concurrent bool) (BatchRenderer, error)

type Camera

type Camera interface {
	ProjectionMatrix() [16]float32
	GLRect() image.Rectangle
}

type Drawable

type Drawable interface {
	Bind()               // Bind calls gl.BindTexture(gl.GL_TEXTURE_2D, ...)
	NativeID() uint32    // OpenGL handle of the associated texture
	Origin() image.Point // Point of origin
	Size() image.Point   // Drawable size
	UV() [4]float32      // UV coordinates of the drawable in the associated texture
}

Drawable wraps the methods of drawable objects like texture.Texture and texture.Region.

type FrameBuffer

type FrameBuffer interface {
	Size() image.Point
	View() *View
}

FrameBuffer represents a render target framebuffer.

type Hinting

type Hinting int

Hinting selects how to quantize a vector font's glyph nodes.

Not all fonts support hinting.

This is a convenience duplicate of golang.org/x/image/font#Hinting

type OrgPosition

type OrgPosition int

OrgPosition determines the on-screen position of the point of origin of the view.

const (
	OrgTopLeft OrgPosition = iota
	OrgCenter
)

type Point

type Point struct {
	X float32
	Y float32
}

func FbToGL

func FbToGL(fb FrameBuffer, p Point) Point

FbToGL converts framebuffer pixel coordinates to GL coordinates in range [-1, 1].

func GLToFb

func GLToFb(fb FrameBuffer, p Point) Point

GLToFb converts GL coordinates in range [-1, 1] to framebuffer pixel coordinates.

func Pt

func Pt(x, y float32) Point

func PtI

func PtI(x, y int) Point

func PtPt

func PtPt(p image.Point) Point

func (Point) Add

func (p Point) Add(pt Point) Point

func (Point) Div

func (p Point) Div(k float32) Point

func (Point) Eq

func (p Point) Eq(pt Point) bool

func (Point) In

func (p Point) In(r image.Rectangle) bool

func (Point) Mul

func (p Point) Mul(k float32) Point

func (Point) String

func (p Point) String() string

func (Point) Sub

func (p Point) Sub(pt Point) Point

type Region

type Region struct {
	*Texture
	// contains filtered or unexported fields
}

Region is a Drawable that represents a sub-region in a Texture or another Region.

func (*Region) Origin

func (r *Region) Origin() image.Point

Origin retruns the point of origin of the region.

func (*Region) Rect

func (r *Region) Rect() image.Rectangle

Rect returns the region's bounding rectangle within the parent texture.

func (*Region) Region

func (r *Region) Region(bounds image.Rectangle, origin image.Point) *Region

Region returns a sub-region within the Region.

func (*Region) Size

func (r *Region) Size() image.Point

Size retruns the size of the region.

func (*Region) UV

func (r *Region) UV() [4]float32

UV returns the regions's UV coordinates in the range [0, 1]

type Renderer

type Renderer interface {
	Draw(d Drawable, dp, scale Point, rot float32, c color.Color)
	Camera(Camera)
	Clear(color.Color)
}

type Screen

type Screen struct {
	// contains filtered or unexported fields
}

A Screen is a FrameBuffer implementation for a physical display screen.

func NewScreen

func NewScreen(sz image.Point) *Screen

NewScreen returns a new screen of the requested size. The size should be updated whenever the size of the associated frame buffer changes.

func (*Screen) SetSize

func (s *Screen) SetSize(sz image.Point)

SetSize sets the Screen size to sz.

func (*Screen) Size

func (s *Screen) Size() image.Point

Size returns the screen size.

func (*Screen) View

func (s *Screen) View() *View

View returns the fullscreen view for that screen. The parent screen changes the view Rect whenever the Screen size is changed. Client code is free to adjust the view Origin, Angle and Scale.

type TextDrawer

type TextDrawer struct {
	// contains filtered or unexported fields
}

TextDrawer draws text.

A Drawer is not safe for concurrent use by multiple goroutines, since its Face is not.

func NewTextDrawer

func NewTextDrawer(f font.Face, magFilter TextureFilter) *TextDrawer

NewTextDrawer returns a new TextDrawer using the given font face. The magFilter is the texture filter used when up-scaling.

func (*TextDrawer) BoundBytes

func (d *TextDrawer) BoundBytes(s []byte) (dot image.Point, size image.Point, advance float32)

BoundBytes returns the draw point and pixel size of s, as well as the advance.

It is equivalent to BoundString(string(s)) but may be more efficient.

func (*TextDrawer) BoundString

func (d *TextDrawer) BoundString(s string) (dot image.Point, size image.Point, advance float32)

BoundString returns the draw point and pixel size of s, as well as the advance.

func (*TextDrawer) DrawBytes

func (d *TextDrawer) DrawBytes(batch Renderer, s []byte, dp, scale Point, c color.Color) (advance float32)

DrawBytes uses the provided batch to draw s at coordinates x, y with the given color. It returns the advance.

It is equivalent to DrawString(b, x, y, string(s), c) but may be more efficient.

func (*TextDrawer) DrawString

func (d *TextDrawer) DrawString(batch Renderer, s string, dp, scale Point, c color.Color) (advance float32)

DrawString uses the provided batch to draw s at coordinates x, y with the given color. It returns the advance.

func (*TextDrawer) Face

func (d *TextDrawer) Face() font.Face

func (*TextDrawer) Glyph

func (d *TextDrawer) Glyph(dot fixed.Point26_6, r rune) (dp image.Point, gr *Region, advance fixed.Int26_6)

Glyph returns the glyph texture Region for rune r drawn at dot, the draw point (for batch.Draw) as well as the advance.

func (*TextDrawer) MeasureBytes

func (d *TextDrawer) MeasureBytes(s []byte) (advance float32)

MeasureBytes returns how far dot would advance by drawing s.

func (*TextDrawer) MeasureString

func (d *TextDrawer) MeasureString(s string) (advance float32)

MeasureString returns how far dot would advance by drawing s.

type Texture

type Texture struct {
	// contains filtered or unexported fields
}

A Texture is a Drawable that represents an OpenGL texture.

func NewTexture

func NewTexture(width, height int, params ...TextureParameter) *Texture

NewTexture Returns a new uninitialized texture of the given width and height.

func TextureFromImage

func TextureFromImage(src image.Image, params ...TextureParameter) *Texture

TextureFromImage creates a new texture of the same dimensions as the source image. Regardless of the source image type, the resulting texture is always in RGBA format.

func (*Texture) Bind

func (t *Texture) Bind()

Bind binds the texture in the OpenGL context.

func (*Texture) Delete

func (t *Texture) Delete()

Delete deletes the texture.

func (*Texture) GLCoords

func (t *Texture) GLCoords(pt Point) Point

GLCoords return the coordinates of the point pt mapped to the range [0, 1].

func (*Texture) NativeID

func (t *Texture) NativeID() uint32

NativeID returns the native identifier of the texture.

func (*Texture) Origin

func (t *Texture) Origin() image.Point

Origin retruns the point of origin of the texture.

func (*Texture) Parameters

func (t *Texture) Parameters(params ...TextureParameter)

Parameters sets the given texture parameters.

func (*Texture) Region

func (t *Texture) Region(bounds image.Rectangle, origin image.Point) *Region

Region returns a region within the texture.

func (*Texture) SetSubImage

func (t *Texture) SetSubImage(dr image.Rectangle, src image.Image, sp image.Point)

SetSubImage draws src to the texture. It works identically to draw.Draw with op set to draw.Src.

func (*Texture) Size

func (t *Texture) Size() image.Point

Size returns the size of the texture.

func (*Texture) UV

func (t *Texture) UV() [4]float32

UV returns the texture's UV coordinates in the range [0, 1]

type TextureFilter

type TextureFilter int32

TextureFilter selects how to filter textures when minifying or magnifying.

const (
	Nearest TextureFilter = gl.GL_NEAREST
	Linear                = gl.GL_LINEAR
)

FilterMode values map directly to their OpenGL equivalents.

type TextureParameter

type TextureParameter interface {
	// contains filtered or unexported methods
}

TextureParameter is implemented by functions setting texture parameters. See New.

func Filter

func Filter(min, mag TextureFilter) TextureParameter

Filter sets the GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER texture parameters.

func Wrap

func Wrap(wrapS, wrapT TextureWrap) TextureParameter

Wrap sets the GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T texture parameters.

type TextureWrap

type TextureWrap int32

TextureWrap selects how textures wrap when texture coordinates get outside of the range [0, 1].

When used in conjunction with github.com/db47h/grog/batch, the only settings that make sense are ClampToEdge (the default) and ClampToBorder.

type View

type View struct {
	// Parent FrameBuffer
	Fb FrameBuffer
	// View rectangle in framebuffer pixel coordinates.
	Rect image.Rectangle
	// World coordinates of the point of origin of the view.
	Origin Point
	// On-screen position of the view's point of origin. View rotation and
	// scaling will preserve this property.
	OrgPos OrgPosition
	// View scaling factor.
	Scale float32
	// View angle. Due to Y axis pointing down, positive angles correspond to a
	// clockwise rotation.
	Angle float32
}

A View converts world coordinates to screen coordinates.

// keep track of frame buffer size in event handlers
s := NewScreen(fbWidth, fbHeight)
v := s.RootView()
// mapView will display a minimap in the bottom right corner
mv :=&grog.View{Fb: s, Scale: 1.0}

// draw loop
for {
	batch.Begin()
	v.Origin = player.Pos()
	batch.SetView(v)
	// draw commands
	// ...

	// switch to minimap view
	mv.Rect = image.Rectangle{Min: s.Size().Sub(image.Pt(200,200), Max: s.Size()}
	batch.SetView(mv)
	// draw minimap
	// ...
}

FrameBuffer and View coordinates have the y axis pointing downwards (y = 0 is the top line of the screen).

func (*View) GLRect

func (v *View) GLRect() image.Rectangle

GLRect returns an image.Rectangle for gl.Scissor (y axis upwards, 0,0 in the bottom left corner).

r := v.GLRect()
gl.Scissor(int32(r.Min.X), int32(r.Min.Y), int32(r.Dx()), int32(r.Dy()))

func (*View) Pan

func (v *View) Pan(p Point)

Pan pans the view by p.X, p.Y screen pixels.

This is a shorthand for

v.Origin = v.Origin.Add(v.ScreenVecToWorld(dv))

func (*View) ProjectionMatrix

func (v *View) ProjectionMatrix() [16]float32

ProjectionMatrix returns a 4x4 projection matrix suitable for Batch.SetProjectionMatrix.

func (*View) ScreenToView

func (v *View) ScreenToView(p Point) Point

ScreenToView converts screen coordinates to view coordinates. It is equivalent to p.Sub(v.Rect.Min).

func (*View) ScreenToWorld

func (v *View) ScreenToWorld(p Point) Point

ScreenToWorld converts screen coordinates to world coordinates.

func (*View) ScreenVecToWorld

func (v *View) ScreenVecToWorld(p Point) Point

ScreenVecToWorld converts the vector represented by p in screen coordinates to world coordinates.

This is an optimized version of v.ScreenToWorld(p).Sub(v.ScreenToWorld(Point{}))

func (*View) Size

func (v *View) Size() image.Point

Size returns the view size in pixels.

func (*View) ViewToScreen

func (v *View) ViewToScreen(p Point) Point

ViewToScreen converts view coordinates to screen coordinates. It is equivalent to p.Add(v.Rect.Min).

func (*View) ViewToWorld

func (v *View) ViewToWorld(p Point) Point

ViewToWorld converts view coordinates to world coordinates.

func (*View) WorldToScreen

func (v *View) WorldToScreen(p Point) Point

WorldToScreen converts world coordinates to screen coordinates.

func (*View) WorldToView

func (v *View) WorldToView(p Point) Point

WorldToView converts world coordinates to view coordinates.

func (*View) WorldVecToScreen

func (v *View) WorldVecToScreen(p Point) Point

WorldVecToScreen converts the vector represented by p in world coordinates to screen coordinates.

This is an optimized version of v.WorldToScreen(p).Sub(v.WorldToScreen(Point{}))

Directories

Path Synopsis
cmd
The loop package provides a simple fixed-timestep event loop.
The loop package provides a simple fixed-timestep event loop.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL