diff --git a/README.md b/README.md index 9132c1c..c5e4d4f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,11 @@ Game engine written in Go using OpenGL and GLFW. Mostly for 2D rendering, but al ## Install +go-game requires OpenGL and GLFW. The following three steps install everything you need: + ``` +go get github.com/go-gl/gl/v4.5-core/gl +go get github.com/go-gl/glfw/v3.1/glfw go get github.com/DeKugelschieber/go-game ``` @@ -23,3 +27,7 @@ You can find some examples within the demo folder. For full reference visit: htt - 3.1 To use an older GL version, you need to replace the GL imports in package goga. It should mostly be compatible down to 3.x. + +## License + +MIT diff --git a/demo/model/assets/cube.ply b/demo/model/assets/cube.ply new file mode 100644 index 0000000..fda7427 --- /dev/null +++ b/demo/model/assets/cube.ply @@ -0,0 +1,53 @@ +ply +format ascii 1.0 +comment Created by Blender 2.69 (sub 0) - www.blender.org, source file: '' +element vertex 26 +property float x +property float y +property float z +property float nx +property float ny +property float nz +property float s +property float t +element face 12 +property list uchar uint vertex_indices +end_header +1.000000 1.000000 -1.000000 0.000000 0.000000 -1.000000 0.499999 0.500000 +1.000000 -1.000000 -1.000000 0.000000 0.000000 -1.000000 0.499999 0.749956 +-1.000000 -1.000000 -1.000000 0.000000 0.000000 -1.000000 0.250043 0.749957 +1.000000 0.999999 1.000000 -0.000000 -0.000000 1.000000 0.499998 0.250043 +-1.000000 1.000000 1.000000 -0.000000 -0.000000 1.000000 0.250041 0.250044 +0.999999 -1.000001 1.000000 -0.000000 -0.000000 1.000000 0.499996 0.000087 +1.000000 1.000000 -1.000000 1.000000 0.000000 -0.000000 0.749956 0.749956 +1.000000 0.999999 1.000000 1.000000 0.000000 -0.000000 0.749956 0.999913 +1.000000 -1.000000 -1.000000 1.000000 0.000000 -0.000000 0.499999 0.749956 +1.000000 -1.000000 -1.000000 -0.000000 -1.000000 -0.000000 0.499999 0.749956 +0.999999 -1.000001 1.000000 -0.000000 -1.000000 -0.000000 0.500000 0.999913 +-1.000000 -1.000000 -1.000000 -0.000000 -1.000000 -0.000000 0.250043 0.749957 +-1.000000 -1.000000 -1.000000 -1.000000 0.000000 -0.000000 0.250043 0.749957 +-1.000000 -1.000000 1.000000 -1.000000 0.000000 -0.000000 0.250043 0.999913 +-1.000000 1.000000 1.000000 -1.000000 0.000000 -0.000000 0.000087 0.999913 +1.000000 0.999999 1.000000 0.000000 1.000000 0.000000 0.499998 0.250043 +1.000000 1.000000 -1.000000 0.000000 1.000000 0.000000 0.499999 0.500000 +-1.000000 1.000000 1.000000 0.000000 1.000000 0.000000 0.250041 0.250044 +-1.000000 1.000000 -1.000000 0.000000 0.000000 -1.000000 0.250042 0.500001 +1.000000 0.999999 1.000000 1.000000 -0.000001 0.000000 0.749956 0.999913 +0.999999 -1.000001 1.000000 1.000000 -0.000001 0.000000 0.500000 0.999913 +1.000000 -1.000000 -1.000000 1.000000 -0.000001 0.000000 0.499999 0.749956 +-1.000000 1.000000 -1.000000 0.000000 1.000000 0.000000 0.250042 0.500001 +-1.000000 -1.000000 1.000000 0.000000 -0.000000 1.000000 0.250040 0.000088 +-1.000000 1.000000 -1.000000 -1.000000 0.000000 -0.000000 0.000087 0.749957 +-1.000000 -1.000000 1.000000 -0.000000 -1.000000 0.000000 0.250043 0.999913 +3 0 1 2 +3 3 4 5 +3 6 7 8 +3 9 10 11 +3 12 13 14 +3 15 16 17 +3 18 0 2 +3 19 20 21 +3 16 22 17 +3 4 23 5 +3 24 12 14 +3 10 25 11 diff --git a/demo/model/assets/cube.png b/demo/model/assets/cube.png new file mode 100644 index 0000000..530cdd4 Binary files /dev/null and b/demo/model/assets/cube.png differ diff --git a/game.go b/game.go index 4eb608f..fff4588 100644 --- a/game.go +++ b/game.go @@ -22,14 +22,15 @@ const ( Default_shader_2D_model = "m" Default_shader_2D_tex = "tex" + // source for 2D shader default_shader_2d_vertex_src = `#version 130 uniform mat3 o, m; in vec2 vertex; in vec2 texCoord; out vec2 tc; void main(){ - tc = texCoord; - gl_Position = vec4(o*m*vec3(vertex, 1.0), 1.0); + tc = texCoord; + gl_Position = vec4(o*m*vec3(vertex, 1.0), 1.0); }` default_shader_2d_fragment_src = `#version 130 precision highp float; @@ -37,7 +38,61 @@ const ( in vec2 tc; out vec4 color; void main(){ - color = texture(tex, tc); + color = texture(tex, tc); + }` + + // constants for default 3D shader + Default_shader_3D_vertex_attrib = "vertex" + Default_shader_3D_texcoord_attrib = "texCoord" + Default_shader_3D_pv = "pv" + Default_shader_3D_model = "m" + Default_shader_3D_tex = "tex" + + // source for 3D shader + default_shader_3d_vertex_src = `#version 130 + uniform mat4 pv, m; + in vec3 vertex; + in vec2 texCoord; + out vec2 tc; + void main(){ + tc = texCoord; + gl_Position = pv*m*vec4(vertex, 1.0); + }` + default_shader_3d_fragment_src = `#version 130 + precision highp float; + uniform sampler2D tex; + in vec2 tc; + out vec4 color; + void main(){ + color = texture(tex, tc); + }` + + // constants for default text shader + Default_shader_text_vertex_attrib = "vertex" + Default_shader_text_texcoord_attrib = "texCoord" + Default_shader_text_ortho = "o" + Default_shader_text_model = "m" + Default_shader_text_tex = "tex" + Default_shader_text_color = "color" + + // source for text shader + default_shader_text_vertex_src = `#version 130 + uniform mat3 o, m; + in vec2 vertex; + in vec2 texCoord; + out vec2 tc; + void main(){ + tc = texCoord; + gl_Position = vec4(o*m*vec3(vertex, 1.0), 1.0); + }` + default_shader_text_fragment_src = `#version 130 + precision highp float; + uniform sampler2D tex; + uniform vec4 color; + in vec2 tc; + out vec4 c; + void main(){ + c = texture(tex, tc)*color; }` ) @@ -73,8 +128,10 @@ var ( viewportHeight int // Default resources - DefaultCamera *Camera - Default2DShader *Shader + DefaultCamera *Camera + Default2DShader *Shader + Default3DShader *Shader + DefaultTextShader *Shader ) func init() { @@ -211,7 +268,7 @@ func initGoga(width, height int) { DefaultCamera.CalcRatio() DefaultCamera.CalcOrtho() - // default shader + // default 2D shader shader, err := NewShader(default_shader_2d_vertex_src, default_shader_2d_fragment_src) if err != nil { @@ -222,25 +279,52 @@ func initGoga(width, height int) { Default2DShader.BindAttrib(Default_shader_2D_vertex_attrib) Default2DShader.BindAttrib(Default_shader_2D_texcoord_attrib) + // default 3D shader + shader, err = NewShader(default_shader_3d_vertex_src, default_shader_3d_fragment_src) + + if err != nil { + panic(err) + } + + Default3DShader = shader + Default3DShader.BindAttrib(Default_shader_3D_vertex_attrib) + Default3DShader.BindAttrib(Default_shader_3D_texcoord_attrib) + + // default text shader + shader, err = NewShader(default_shader_text_vertex_src, default_shader_text_fragment_src) + + if err != nil { + panic(err) + } + + DefaultTextShader = shader + DefaultTextShader.BindAttrib(Default_shader_text_vertex_attrib) + DefaultTextShader.BindAttrib(Default_shader_text_texcoord_attrib) + // settings and registration ClearColorBuffer(true) EnableAlphaBlending(true) AddLoader(&PngLoader{gl.LINEAR, false}) AddLoader(&PlyLoader{gl.STATIC_DRAW}) AddSystem(NewSpriteRenderer(nil, nil, false)) + AddSystem(NewModelRenderer(nil, nil, false)) AddSystem(NewCulling2D(0, 0, width, height)) } func cleanup() { // cleanup resources - log.Printf("Cleaning up %v resources", len(resources)) + log.Printf("Trying to cleaning up %v resources", len(resources)) + dropped := 0 for _, res := range resources { if drop, ok := res.(Dropable); ok { drop.Drop() + dropped++ } } + log.Printf("Dropped %v resources", dropped) + // cleanup systems log.Printf("Cleaning up %v systems", len(systems)) @@ -259,6 +343,7 @@ func cleanup() { log.Print("Cleaning up default resources") Default2DShader.Drop() + DefaultTextShader.Drop() } // Stops the game and closes the window. @@ -287,6 +372,15 @@ func ClearDepthBuffer(do bool) { } } +func removeClearBuffer(buffer uint32) { + for i, b := range clearBuffer { + if b == buffer { + clearBuffer = append(clearBuffer[:i], clearBuffer[i+1:]...) + return + } + } +} + // Enables/Disables alpha blending by source alpha channel. // BLEND = SRC_ALPHA | ONE_MINUS_SRC_ALPHA func EnableAlphaBlending(enable bool) { @@ -332,12 +426,3 @@ func GetWidth() int { func GetHeight() int { return viewportHeight } - -func removeClearBuffer(buffer uint32) { - for i, buffer := range clearBuffer { - if buffer == buffer { - clearBuffer = append(clearBuffer[:i], clearBuffer[i+1:]...) - return - } - } -} diff --git a/loader.go b/loader.go index ebd602e..835acb8 100644 --- a/loader.go +++ b/loader.go @@ -75,6 +75,25 @@ type Ply struct { IndexBuffer, VertexBuffer, TexCoordBuffer, NormalBuffer *VBO } +// Drops contained GL buffers. +func (p *Ply) Drop() { + if p.IndexBuffer != nil { + p.IndexBuffer.Drop() + } + + if p.VertexBuffer != nil { + p.VertexBuffer.Drop() + } + + if p.TexCoordBuffer != nil { + p.TexCoordBuffer.Drop() + } + + if p.NormalBuffer != nil { + p.NormalBuffer.Drop() + } +} + // Returns the name of this resource. func (p *Ply) GetName() string { return p.name diff --git a/model.go b/model.go new file mode 100644 index 0000000..a66324d --- /dev/null +++ b/model.go @@ -0,0 +1,193 @@ +package goga + +import ( + "github.com/go-gl/gl/v4.5-core/gl" +) + +const ( + model_renderer_name = "modelRenderer" +) + +// Component representing a 3D mesh. +type Mesh struct { + Index, Vertex, TexCoord *VBO + Vao *VAO +} + +// Creates a new mesh with given GL buffers. +// The VAO must be prepared by ModelRenderer. +func NewMesh(index, vertex, texcoord *VBO) *Mesh { + mesh := &Mesh{} + mesh.Index = index + mesh.Vertex = vertex + mesh.TexCoord = texcoord + + CheckGLError() + + return mesh +} + +// Drops the VBOs and VAO contained in mesh. +// This must not be done, if mesh was filled from outer source (like a ply file). +func (m *Mesh) Drop() { + m.Index.Drop() + m.Vertex.Drop() + m.TexCoord.Drop() + m.Vao.Drop() +} + +// Model is an actor having a 3D position, a texture and a 3D mesh. +type Model struct { + *Actor + *Pos3D + *Tex + *Mesh +} + +// Creates a new model with given mesh and texture. +func NewModel(mesh *Mesh, tex *Tex) *Model { + model := &Model{} + model.Actor = NewActor() + model.Pos3D = NewPos3D() + model.Tex = tex + model.Mesh = mesh + model.Size = Vec3{1, 1, 1} + model.Scale = Vec3{1, 1, 1} + model.Visible = true + + CheckGLError() + + return model +} + +// The model renderer is a system rendering models. +// It has a 3D position component, to move all models at once. +type ModelRenderer struct { + Pos3D + + Shader *Shader + Camera *Camera + ortho bool + + models []Model +} + +// Creates a new model renderer using given shader and camera. +// If shader and/or camera are nil, the default one will be used. +// Orth can be set to true, to use orthogonal projection. +func NewModelRenderer(shader *Shader, camera *Camera, ortho bool) *ModelRenderer { + if shader == nil { + shader = Default3DShader + } + + if camera == nil { + camera = DefaultCamera + } + + renderer := &ModelRenderer{} + renderer.Shader = shader + renderer.Camera = camera + renderer.ortho = ortho + renderer.models = make([]Model, 0) + renderer.Size = Vec3{1, 1, 1} + renderer.Scale = Vec3{1, 1, 1} + + CheckGLError() + + return renderer +} + +func (s *ModelRenderer) Cleanup() {} + +// Prepares a model to be rendered by setting up its VAO. +func (s *ModelRenderer) Prepare(model *Model) { + model.Vao = NewVAO() + model.Vao.Bind() + s.Shader.EnableVertexAttribArrays() + model.Index.Bind() + model.Vertex.Bind() + model.Vertex.AttribPointer(s.Shader.GetAttribLocation(Default_shader_3D_vertex_attrib), 3, gl.FLOAT, false, 0) + model.TexCoord.Bind() + model.TexCoord.AttribPointer(s.Shader.GetAttribLocation(Default_shader_3D_texcoord_attrib), 2, gl.FLOAT, false, 0) + model.Vao.Unbind() +} + +// Adds model to the renderer. +// Perpare it first! +func (s *ModelRenderer) Add(actor *Actor, pos *Pos3D, tex *Tex, mesh *Mesh) bool { + id := actor.GetId() + + for _, model := range s.models { + if id == model.Actor.GetId() { + return false + } + } + + s.models = append(s.models, Model{actor, pos, tex, mesh}) + + return true +} + +// Removes model from renderer. +func (s *ModelRenderer) Remove(actor *Actor) bool { + return s.RemoveById(actor.GetId()) +} + +// Removes model from renderer by ID. +func (s *ModelRenderer) RemoveById(id ActorId) bool { + for i, model := range s.models { + if model.Actor.GetId() == id { + s.models = append(s.models[:i], s.models[i+1:]...) + return true + } + } + + return false +} + +// Removes all sprites. +func (s *ModelRenderer) RemoveAll() { + s.models = make([]Model, 0) +} + +// Returns number of sprites. +func (s *ModelRenderer) Len() int { + return len(s.models) +} + +func (s *ModelRenderer) GetName() string { + return model_renderer_name +} + +// Render models. +func (s *ModelRenderer) Update(delta float64) { + s.Shader.Bind() + s.Shader.SendUniform1i(Default_shader_3D_tex, 0) + + if s.ortho { + s.Shader.SendMat4(Default_shader_3D_pv, *MultMat4(s.Camera.CalcOrtho3D(), s.CalcModel())) + } else { + pv := s.Camera.CalcProjection() + pv.Mult(s.Camera.CalcView()) + s.Shader.SendMat4(Default_shader_3D_pv, *pv) + } + + var tid uint32 + + for i := range s.models { + if !s.models[i].Visible { + continue + } + + s.Shader.SendMat4(Default_shader_3D_model, *s.models[i].CalcModel()) + s.models[i].Vao.Bind() + + // prevent texture switching when not neccessary + if tid != s.models[i].Tex.GetId() { + tid = s.models[i].Tex.GetId() + s.models[i].Tex.Bind() + } + + gl.DrawElements(gl.TRIANGLES, s.models[i].Index.Size(), gl.UNSIGNED_INT, nil) + } +} diff --git a/res_util.go b/res_util.go index d4dd0e6..26a0b8b 100644 --- a/res_util.go +++ b/res_util.go @@ -21,3 +21,21 @@ func GetTex(name string) (*Tex, error) { return tex, nil } + +// Finds and returns a Ply resource. +// If not found or when the resource is of wrong type, an error will be returned. +func GetPly(name string) (*Ply, error) { + res := GetResByName(name) + + if res == nil { + return nil, errors.New("Resource not found") + } + + ply, ok := res.(*Ply) + + if !ok { + return nil, errors.New("Resource was not of type *Ply") + } + + return ply, nil +} diff --git a/shader/basic2D.fs b/shader/basic2D.fs deleted file mode 100644 index 71bd6c0..0000000 --- a/shader/basic2D.fs +++ /dev/null @@ -1,12 +0,0 @@ -#version 130 -precision highp float; - -uniform sampler2D tex; - -in vec2 tc; - -out vec4 color; - -void main(){ - color = texture(tex, tc); -} diff --git a/shader/basic2D.vs b/shader/basic2D.vs deleted file mode 100644 index 1076653..0000000 --- a/shader/basic2D.vs +++ /dev/null @@ -1,13 +0,0 @@ -#version 130 - -uniform mat3 o, m; - -in vec2 vertex; -in vec2 texCoord; - -out vec2 tc; - -void main(){ - tc = texCoord; - gl_Position = vec4(o*m*vec3(vertex, 1.0), 1.0); -} diff --git a/shader/basic3D.fs b/shader/basic3D.fs deleted file mode 100644 index 71bd6c0..0000000 --- a/shader/basic3D.fs +++ /dev/null @@ -1,12 +0,0 @@ -#version 130 -precision highp float; - -uniform sampler2D tex; - -in vec2 tc; - -out vec4 color; - -void main(){ - color = texture(tex, tc); -} diff --git a/shader/basic3D.vs b/shader/basic3D.vs deleted file mode 100644 index 1d23790..0000000 --- a/shader/basic3D.vs +++ /dev/null @@ -1,13 +0,0 @@ -#version 130 - -uniform mat4 pv, m; - -in vec3 vertex; -in vec2 texCoord; - -out vec2 tc; - -void main(){ - tc = texCoord; - gl_Position = pv*m*vec4(vertex, 1.0); -} diff --git a/shader/text.fs b/shader/text.fs deleted file mode 100644 index 5618a3a..0000000 --- a/shader/text.fs +++ /dev/null @@ -1,13 +0,0 @@ -#version 130 -precision highp float; - -uniform sampler2D tex; -uniform vec4 color; - -in vec2 tc; - -out vec4 c; - -void main(){ - c = texture(tex, tc)*color; -} diff --git a/shader/text.vs b/shader/text.vs deleted file mode 100644 index 1076653..0000000 --- a/shader/text.vs +++ /dev/null @@ -1,13 +0,0 @@ -#version 130 - -uniform mat3 o, m; - -in vec2 vertex; -in vec2 texCoord; - -out vec2 tc; - -void main(){ - tc = texCoord; - gl_Position = vec4(o*m*vec3(vertex, 1.0), 1.0); -} diff --git a/sprite.go b/sprite.go index 2d1c3e1..5dc8ba8 100644 --- a/sprite.go +++ b/sprite.go @@ -15,19 +15,6 @@ type Sprite struct { *Tex } -// The sprite renderer is a system rendering sprites. -// It has a 2D position componente, to move all sprites at once. -type SpriteRenderer struct { - Pos2D - - Shader *Shader - Camera *Camera - - sprites []Sprite - index, vertex, texCoord *VBO - vao *VAO -} - // Creates a new sprite with given texture. func NewSprite(tex *Tex) *Sprite { sprite := &Sprite{} @@ -43,6 +30,19 @@ func NewSprite(tex *Tex) *Sprite { return sprite } +// The sprite renderer is a system rendering sprites. +// It has a 2D position component, to move all sprites at once. +type SpriteRenderer struct { + Pos2D + + Shader *Shader + Camera *Camera + + sprites []Sprite + index, vertex, texCoord *VBO + vao *VAO +} + // Creates a new sprite renderer using given shader and camera. // If shader and/or camera are nil, the default one will be used. func NewSpriteRenderer(shader *Shader, camera *Camera, flip bool) *SpriteRenderer { @@ -132,7 +132,7 @@ func (s *SpriteRenderer) GetName() string { return sprite_renderer_name } -// Renders sprites. +// Render sprites. func (s *SpriteRenderer) Update(delta float64) { s.Shader.Bind() s.Shader.SendMat3(Default_shader_2D_ortho, *MultMat3(s.Camera.CalcOrtho(), s.CalcModel())) @@ -147,6 +147,7 @@ func (s *SpriteRenderer) Update(delta float64) { s.Shader.SendMat3(Default_shader_2D_model, *s.sprites[i].CalcModel()) + // prevent texture switching when not neccessary if tid != s.sprites[i].Tex.GetId() { tid = s.sprites[i].Tex.GetId() s.sprites[i].Tex.Bind() diff --git a/text.go b/text.go new file mode 100644 index 0000000..b213882 --- /dev/null +++ b/text.go @@ -0,0 +1,402 @@ +package goga + +/* +import ( + "core" + "dp" + "encoding/json" + "geo" + "github.com/go-gl/gl/v4.5-core/gl" + "io/ioutil" + "util" +) + +const ( + char_padding = 2 +) + +// Returns a new renderable text object. +func NewText(font *Font, text string) *Text { + t := &Text{id: core.NextId()} + t.text = text + t.index = dp.NewVBO(gl.ELEMENT_ARRAY_BUFFER) + t.vertex = dp.NewVBO(gl.ARRAY_BUFFER) + t.texCoord = dp.NewVBO(gl.ARRAY_BUFFER) + t.vao = dp.NewVAO() + t.SetText(font, text) + t.Size = geo.Vec2{1, 1} + t.Scale = geo.Vec2{1, 1} + t.Visible = true + + return t +} + +type Character struct { + char byte + min, max, size geo.Vec2 + offset float64 +} + +type Font struct { + Tex *dp.Tex + tileSize float64 + CharPadding geo.Vec2 + Space, Tab, Line float64 + chars []Character +} + +type jsonChar struct { + Char string + X, Y, Offset float64 +} + +// Creates a new font from texture. Characters must be added afterwards. +// The characters must be placed within a grid, +// the second parameter describes the width and height of one tile in pixel. +func NewFont(tex *dp.Tex, tileSize int) *Font { + font := &Font{} + font.Tex = tex + font.tileSize = float64(tileSize) + font.CharPadding = geo.Vec2{0.05, 0.05} + font.Space = 0.3 + font.Tab = 1.2 + font.Line = 1 + font.chars = make([]Character, 0) + + return font +} + +// Loads characters from JSON file. +// Format: +// +// [ +// { +// "char": "a", +// "x": 0, +// "y": 0, +// "offset": 0 +// }, +// ... +// ] +// +// Where x and y start in the upper left corner of the texture, both of type int. +// Offset is optional and can be used to move a character up or down (relative to others). +// If cut is set to true, the characters will be true typed. +func (f *Font) LoadFromJson(path string, cut bool) error { + // load file content + content, err := ioutil.ReadFile(path) + + if err != nil { + return err + } + + // read json + chars := make([]jsonChar, 0) + + if err = json.Unmarshal(content, &chars); err != nil { + return err + } + + f.extractChars(chars, cut) + + return nil +} + +func (f *Font) extractChars(chars []jsonChar, cut bool) { + for _, char := range chars { + if len(char.Char) != 1 { + continue + } + + var min, max, size geo.Vec2 + + if !cut { + min = geo.Vec2{char.X * f.tileSize, char.Y * f.tileSize} + max = geo.Vec2{min.X + f.tileSize, min.Y + f.tileSize} + size = geo.Vec2{1, 1} + } else { + min, max, size = f.cutChar(int(char.X), int(char.Y)) + } + + f.chars = append(f.chars, Character{char.Char[0], min, max, size, char.Offset}) + } +} + +func (f *Font) cutChar(x, y int) (geo.Vec2, geo.Vec2, geo.Vec2) { + minX := int(f.Tex.GetSize().X) + minY := int(f.Tex.GetSize().Y) + maxX := 0 + maxY := 0 + rgba := f.Tex.GetRGBA() + + for ry := y * int(f.tileSize); ry < (y+1)*int(f.tileSize); ry++ { + for rx := x * int(f.tileSize); rx < (x+1)*int(f.tileSize); rx++ { + _, _, _, a := rgba.At(rx, ry).RGBA() + + if a == 0 { + continue + } + + if rx < minX { + minX = rx + } else if rx > maxX { + maxX = rx + } + + if ry < minY { + minY = ry + } else if ry > maxY { + maxY = ry + } + } + } + + minX -= char_padding + maxX += char_padding + minY -= char_padding + maxY += char_padding + + texSize := f.Tex.GetSize() + min := geo.Vec2{float64(minX) / texSize.X, float64(maxY) / texSize.Y} + max := geo.Vec2{float64(maxX) / texSize.X, float64(minY) / texSize.Y} + + // size + size := geo.Vec2{float64(maxX-minX) / f.tileSize, float64(maxY-minY) / f.tileSize} + + return min, max, size +} + +func (f *Font) getChar(char byte) *Character { + for _, character := range f.chars { + if character.char == char { + return &character + } + } + + return nil +} + +type Text struct { + *Actor + *Pos2D + + id int + text string + bounds geo.Vec2 + + index, vertex, texCoord *dp.VBO + vao *dp.VAO +} + +// Deletes GL buffers bound to this text. +func (t *Text) Drop() { + t.index.Drop() + t.vertex.Drop() + t.texCoord.Drop() + t.vao.Drop() +} + +// Sets the given string as text and (re)creates buffers. +func (t *Text) SetText(font *Font, text string) { + t.text = text + + indices := make([]uint32, len(text)*6) + vertices := make([]float32, len(text)*8) + texCoords := make([]float32, len(text)*8) + chars := 0 + + // create indices + var index uint32 = 0 + + for i := 0; i < len(text)*6; i += 6 { + indices[i] = index + indices[i+1] = index + 1 + indices[i+2] = index + 2 + indices[i+3] = index + 1 + indices[i+4] = index + 2 + indices[i+5] = index + 3 + + index += 4 + } + + // create vertices/texCoords + index = 0 + offset := geo.Vec2{} + var width, height float64 + + for i := 0; i < len(text)*8 && int(index) < len(text); i += 8 { + c := font.getChar(text[index]) + index++ + + // whitespace and new line + if text[index-1] == ' ' { + offset.X += font.Space + i -= 8 + continue + } + + if text[index-1] == '\n' { + offset.X = 0 + offset.Y -= font.Line + i -= 8 + continue + } + + if text[index-1] == '\t' { + offset.X += font.Tab + i -= 8 + continue + } + + // character not found + if c == nil { + i -= 8 + continue + } + + // usual character + vertices[i] = float32(offset.X) + vertices[i+1] = float32(offset.Y + c.offset) + vertices[i+2] = float32(offset.X + c.size.X) + vertices[i+3] = float32(offset.Y + c.offset) + vertices[i+4] = float32(offset.X) + vertices[i+5] = float32(offset.Y + c.size.Y + c.offset) + vertices[i+6] = float32(offset.X + c.size.X) + vertices[i+7] = float32(offset.Y + c.size.Y + c.offset) + + texCoords[i] = float32(c.min.X) + texCoords[i+1] = float32(c.min.Y) + texCoords[i+2] = float32(c.max.X) + texCoords[i+3] = float32(c.min.Y) + texCoords[i+4] = float32(c.min.X) + texCoords[i+5] = float32(c.max.Y) + texCoords[i+6] = float32(c.max.X) + texCoords[i+7] = float32(c.max.Y) + + offset.X += c.size.X + font.CharPadding.X + chars++ + + if offset.X > width { + width = offset.X + } + + if offset.Y*-1+font.Line > height { + height = offset.Y*-1 + font.Line + } + } + + t.bounds = geo.Vec2{width, height} + + // fill GL buffer + t.index.Fill(gl.Ptr(indices[:chars*6]), 4, chars*6, gl.STATIC_DRAW) + t.vertex.Fill(gl.Ptr(vertices[:chars*8]), 4, chars*8, gl.STATIC_DRAW) + t.texCoord.Fill(gl.Ptr(texCoords[:chars*8]), 4, chars*8, gl.STATIC_DRAW) + + util.CheckGLError() +} + +func (t *Text) GetId() int { + return t.id +} + +// Returns the text as string. +func (t *Text) GetText() string { + return t.text +} + +// Returns bounds of text, which is the size of characters. +func (t *Text) GetBounds() geo.Vec2 { + return geo.Vec2{t.bounds.X * t.Size.X * t.Scale.X, t.bounds.Y * t.Size.Y * t.Scale.Y} +} + +type TextRenderer struct { + Pos2D + + Shader *dp.Shader + Camera *Camera + Font *Font + Color geo.Vec4 + texts []*Text +} + +// Creates a new text renderer using given shader, camera and font. +// If shader and/or camera are nil, the default one will be used. +func NewTextRenderer(shader *dp.Shader, camera *Camera, font *Font) *TextRenderer { + renderer := &TextRenderer{} + renderer.Shader = shader + renderer.Camera = camera + renderer.Font = font + renderer.Color = geo.Vec4{1, 1, 1, 1} + renderer.texts = make([]*Text, 0) + renderer.Size = geo.Vec2{1, 1} + renderer.Scale = geo.Vec2{1, 1} + + return renderer +} + +// Prepares a text for rendering. +func (r *TextRenderer) Prepare(text *Text) { + text.vao = dp.NewVAO() + text.vao.Bind() + r.Shader.EnableVertexAttribArrays() + text.index.Bind() + text.vertex.Bind() + text.vertex.AttribPointer(r.Shader.GetAttribLocation(TEXTRENDERER_VERTEX_ATTRIB), 2, gl.FLOAT, false, 0) + text.texCoord.Bind() + text.texCoord.AttribPointer(r.Shader.GetAttribLocation(TEXTRENDERER_TEXCOORD_ATTRIB), 2, gl.FLOAT, false, 0) + text.vao.Unbind() +} + +// Adds text to the renderer. +func (r *TextRenderer) Add(text *Text) { + r.texts = append(r.texts, text) +} + +// Returns text by ID. +func (r *TextRenderer) Get(id int) *Text { + for _, text := range r.texts { + if text.GetId() == id { + return text + } + } + + return nil +} + +// Removes text from renderer by ID. +func (r *TextRenderer) Remove(id int) *Text { + for i, text := range r.texts { + if text.GetId() == id { + r.texts = append(r.texts[:i], r.texts[i+1:]...) + return text + } + } + + return nil +} + +// Removes all sprites. +func (r *TextRenderer) Clear() { + r.texts = make([]*Text, 0) +} + +// Renders sprites. +func (r *TextRenderer) Render() { + r.Shader.Bind() + r.Shader.SendMat3(TEXTRENDERER_ORTHO, *geo.MultMat3(r.Camera.CalcOrtho(), r.CalcModel())) + r.Shader.SendUniform1i(TEXTRENDERER_TEX, 0) + r.Shader.SendUniform4f(TEXTRENDERER_COLOR, float32(r.Color.X), float32(r.Color.Y), float32(r.Color.Z), float32(r.Color.W)) + r.Font.Tex.Bind() + + for i := range r.texts { + if !r.texts[i].Visible { + continue + } + + r.texts[i].vao.Bind() + r.Shader.SendMat3(TEXTRENDERER_MODEL, *r.texts[i].CalcModel()) + + gl.DrawElements(gl.TRIANGLES, r.texts[i].index.Size(), gl.UNSIGNED_INT, nil) + } +} +*/