Compare commits

..

60 Commits

Author SHA1 Message Date
70dccd757e Run init within an imgui frame 2023-02-03 02:20:50 +04:00
d7cd5bfc8d Run one imgui frame before init and another after init 2023-02-03 01:37:01 +04:00
3b8e5c06de Only initialize video and timer subsystems of sdl 2023-02-03 01:14:57 +04:00
8e9dbee002 Fix build tag 2022-12-06 21:10:06 +04:00
7b2fb19618 Update workflow dependencies 2022-12-06 06:34:06 +04:00
9651f77348 Update github workflow to use go 1.18 2022-12-06 06:23:02 +04:00
a16654107b Implement UpdateAllComps 2022-12-06 06:19:38 +04:00
855cbfaba3 Improve assert+imporve error messages when adding comps 2022-12-06 06:17:25 +04:00
b025afe1b4 Add Init,Update,Destroy to Comp+HasComp,DestroyComp funcs 2022-12-06 06:07:49 +04:00
84cd8c28c8 Add BaseComp 2022-12-06 05:20:20 +04:00
7d5e3e2d82 Move comp into own file 2022-12-06 04:56:17 +04:00
23a6689346 Improve entity flags+add freeListSize to registry 2022-12-06 04:51:03 +04:00
36488ead04 Add skybox textures 2022-12-06 04:29:57 +04:00
de77d5464e Remove todo + upgrade gglm 2022-12-06 04:28:34 +04:00
305982deca Light pos and color controls 2022-12-06 03:48:07 +04:00
653a315631 Remove some old code 2022-10-20 01:28:06 +04:00
c971324b5a Move camera when right mouse button is clicked 2022-10-14 07:59:46 +04:00
b5a2479c16 Skybox demo 2022-10-14 07:55:48 +04:00
6f54aecb5f Loading cubemap textures+don't store texture by default 2022-10-14 05:52:28 +04:00
7a25aea6ba Clear stencil buffer every frame+Depth buffer viz 2022-10-14 04:34:40 +04:00
3071b52c85 Basic submeshe support 2022-10-07 05:50:48 +04:00
1b858bd4ac Update assimp+remove unused funcs 2022-10-01 06:54:23 +04:00
d550767cb6 Allow meshes without UVs 2022-10-01 06:52:47 +04:00
271b1c0cea Space 2022-10-01 01:48:59 +04:00
0da031aa57 TODO 2022-10-01 01:44:22 +04:00
62194c4cad FPS camera with pitch and yaw 2022-10-01 01:43:01 +04:00
bd79f6e274 Update camera to use pos+forward vectors to calc target 2022-09-30 04:17:48 +04:00
ac0ca8ee39 Generational indices+get/free entity+free list 2022-09-24 23:20:08 +04:00
35ff496a9a Starting entities, components, and levels 2022-08-14 22:00:04 +04:00
52b77e017e Camera package+ rename asserts->assert 2022-07-23 22:51:57 +04:00
b85056dd31 Update to imgui-go v4.5.0 2022-07-23 20:18:33 +04:00
e5ea6f986f Add SetDataWithUsage 2022-07-11 11:58:31 +04:00
c4853792a5 Use cached getUniform in imgui 2022-07-11 11:36:26 +04:00
8cf9be2830 Support arrays and slices in buffers.(SetData/SetIndexBufData) 2022-07-03 23:08:20 +04:00
71acc2e9ab Remove comment 2022-07-03 08:41:36 +04:00
2690014fc5 Allow texture loading from in-mem images 2022-07-02 22:54:36 +04:00
fe2aef6b6d Support combined shaders+allow loading shaders from string 2022-07-02 22:49:28 +04:00
d4fe6d4071 Update gglm version 2022-07-02 21:41:44 +04:00
51057b8a0d TextureLoadOptions+fix DT bug+remove SetAttribute 2022-07-02 21:38:48 +04:00
e1bf0697fc Simplify the game interface 2022-07-02 21:21:59 +04:00
901d8e2b5e Don't disable blending after ui render 2022-05-21 15:48:50 +04:00
89d04c9d24 Enable blending by default 2022-05-21 12:11:15 +04:00
f1b6f3a7c0 Start function in Game interface 2022-02-27 11:21:15 +04:00
d1f47316ae Allow imgui within init 2022-02-27 11:11:20 +04:00
709dc062cc Day 15: Basic renderer+improve material system+lockosthread on init 2022-02-26 22:07:59 +04:00
660c41bc06 ensure dt is never zero 2022-02-24 14:34:21 +04:00
99f5548ce2 Fix imgui dt 2022-02-24 14:22:23 +04:00
5a54b1b465 Make all ImguiInfo public 2022-02-24 08:58:25 +04:00
36ac96d641 Allow configuration when loading fonts 2022-02-24 07:50:35 +04:00
577e6250a8 Return imgui font after set 2022-02-24 06:59:19 +04:00
c311a0981c Imgui setfont func 2022-02-24 06:51:24 +04:00
064a932037 Allow app to receive window event callbacks 2022-02-24 05:48:35 +04:00
841a6e989c Remove unused variables 2022-02-23 09:53:09 +04:00
94942e55a1 Cache uniform/attrib locations+display fps averages over a second 2022-02-23 09:05:21 +04:00
15087ac542 Specify RGBA8 as internal opengl format 2022-02-23 07:35:49 +04:00
f16407629a Correct loading of png textures to match opengl coords 2022-02-23 07:28:12 +04:00
592208d5c9 Complete engine game loop+abstract imgui 2022-02-12 22:20:38 +04:00
fd74d58ad3 Debug mode by default 2022-02-07 11:55:29 +04:00
4c2fca48b3 Ignore temp files 2022-02-07 11:47:34 +04:00
50c2ab650f Load texture from res folder 2022-02-05 23:59:41 +04:00
48 changed files with 1903 additions and 724 deletions

View File

@ -1,22 +1,28 @@
name: build-nmage
on:
create:
workflow_dispatch:
jobs:
build-nmage-macos:
runs-on: macos-10.15
runs-on: macos-12
steps:
- name: Install golang 1.17
uses: actions/setup-go@v2
- name: Install golang 1.18
uses: actions/setup-go@v3
with:
go-version: '^1.17'
go-version: '^1.18'
- name: Install assimp-go dylib
run: sudo mkdir -p /usr/local/lib && sudo wget https://github.com/bloeys/assimp-go/releases/download/v0.4.2/libassimp_darwin_amd64.dylib -O /usr/local/lib/libassimp.5.dylib
- name: Install SDL2
run: brew install sdl2{,_image,_mixer,_ttf,_gfx} pkg-config
- name: Clone nmage
run: git clone https://github.com/bloeys/nmage
- name: build nmage
working-directory: nmage
run: go build .

3
.gitignore vendored
View File

@ -14,4 +14,5 @@
# Dependency directories (remove the comment below to include it)
vendor/
.vscode/
imgui.ini
imgui.ini
*~

View File

@ -1,6 +1,6 @@
# nMage
[![build](https://github.com/bloeys/nmage/actions/workflows/run-nmage.yml/badge.svg)](https://github.com/bloeys/nmage/actions/workflows/run-nmage.yml)
[![build](https://github.com/bloeys/nmage/actions/workflows/build-nmage.yml/badge.svg)](https://github.com/bloeys/nmage/actions/workflows/build-nmage.yml)
nMage is a (hopefully!) high performance 3D Game Engine written in Go being developed [live](https://twitch.tv/bloeys), with recordings posted on [YouTube](https://www.youtube.com/channel/UCCf4qyNGPVwpj1HYFGahs_A).

13
assert/assert.go Executable file
View File

@ -0,0 +1,13 @@
package assert
import (
"github.com/bloeys/nmage/consts"
"github.com/bloeys/nmage/logging"
)
func T(check bool, msg string, args ...any) {
if consts.Debug && !check {
logging.ErrLog.Panicf("Assert failed: "+msg, args...)
}
}

View File

@ -1,12 +0,0 @@
package asserts
import (
"github.com/bloeys/nmage/consts"
"github.com/bloeys/nmage/logging"
)
func T(check bool, msg string) {
if consts.Debug && !check {
logging.ErrLog.Panicln("Assert failed:", msg)
}
}

View File

@ -1,27 +1,32 @@
package assets
var (
Textures map[uint32]Texture = make(map[uint32]Texture)
TexturePaths map[string]uint32 = make(map[string]uint32)
Textures = make(map[uint32]Texture)
TexturePaths = make(map[string]uint32)
)
func SetTexture(t Texture) {
func AddTextureToCache(t Texture) {
if _, ok := TexturePaths[t.Path]; ok {
if t.Path != "" {
if _, ok := TexturePaths[t.Path]; ok {
return
}
println("Loaded texture from path:", t.Path)
Textures[t.TexID] = t
TexturePaths[t.Path] = t.TexID
return
}
println("Loaded texture:", t.Path)
Textures[t.TexID] = t
println("Loaded in-mem texture with ID:", t.TexID)
TexturePaths[t.Path] = t.TexID
}
func GetTexture(texID uint32) (Texture, bool) {
func GetTextureFromCacheID(texID uint32) (Texture, bool) {
tex, ok := Textures[texID]
return tex, ok
}
func GetTexturePath(path string) (Texture, bool) {
func GetTextureFromCachePath(path string) (Texture, bool) {
tex, ok := Textures[TexturePaths[path]]
return tex, ok
}

View File

@ -2,9 +2,15 @@ package assets
import (
"bytes"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"strings"
"unsafe"
"github.com/go-gl/gl/v4.1-core/gl"
@ -17,6 +23,7 @@ const (
)
type Texture struct {
//Path only exists for textures loaded from disk
Path string
TexID uint32
Width int32
@ -24,10 +31,34 @@ type Texture struct {
Pixels []byte
}
func LoadPNG(file string) (Texture, error) {
type TextureLoadOptions struct {
TryLoadFromCache bool
WriteToCache bool
GenMipMaps bool
KeepPixelsInMem bool
}
if tex, ok := GetTexturePath(file); ok {
return tex, nil
type Cubemap struct {
// These only exists for textures loaded from disk
RightPath string
LeftPath string
TopPath string
BotPath string
FrontPath string
BackPath string
TexID uint32
}
func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
if loadOptions.TryLoadFromCache {
if tex, ok := GetTextureFromCachePath(file); ok {
return tex, nil
}
}
//Load from disk
@ -42,27 +73,10 @@ func LoadPNG(file string) (Texture, error) {
}
tex := Texture{
Path: file,
Width: int32(img.Bounds().Dx()),
Height: int32(img.Bounds().Dy()),
Pixels: make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4),
Path: file,
}
//NOTE: We only support 8-bit channels (32-bit colors) for now
i := 0
for y := 0; y < img.Bounds().Dy(); y++ {
for x := 0; x < img.Bounds().Dx(); x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
tex.Pixels[i] = c.R
tex.Pixels[i+1] = c.G
tex.Pixels[i+2] = c.B
tex.Pixels[i+3] = c.A
i += 4
}
}
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
@ -71,14 +85,224 @@ func LoadPNG(file string) (Texture, error) {
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
gl.GenerateMipmap(gl.TEXTURE_2D)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
SetTexture(tex)
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
tex := Texture{}
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func LoadTextureJpeg(file string, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
if loadOptions.TryLoadFromCache {
if tex, ok := GetTextureFromCachePath(file); ok {
return tex, nil
}
}
//Load from disk
fileBytes, err := os.ReadFile(file)
if err != nil {
return Texture{}, err
}
img, err := jpeg.Decode(bytes.NewReader(fileBytes))
if err != nil {
return Texture{}, err
}
tex := Texture{
Path: file,
}
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func pixelsFromNrgbaPng(img image.Image) (pixels []byte, width, height int32) {
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
//NOTE: We only support 8-bit channels (32-bit colors) for now
i := 0
width, height = int32(img.Bounds().Dx()), int32(img.Bounds().Dy())
pixels = make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4)
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
for x := 0; x < img.Bounds().Dx(); x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
pixels[i] = c.R
pixels[i+1] = c.G
pixels[i+2] = c.B
pixels[i+3] = c.A
i += 4
}
}
return pixels, width, height
}
func pixelsFromNrgbaJpg(img image.Image) (pixels []byte, width, height int32) {
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
//NOTE: We only support 8-bit channels (32-bit colors) for now
i := 0
width, height = int32(img.Bounds().Dx()), int32(img.Bounds().Dy())
pixels = make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4)
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
for x := 0; x < img.Bounds().Dx(); x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
pixels[i] = c.R
pixels[i+1] = c.G
pixels[i+2] = c.B
pixels[i+3] = c.A
i += 4
}
}
return pixels, width, height
}
func LoadCubemapTextures(rightTex, leftTex, topTex, botTex, frontTex, backTex string) (Cubemap, error) {
var imgDecoder func(r io.Reader) (image.Image, error)
var pixelDecoder func(image.Image) ([]byte, int32, int32)
ext := strings.ToLower(path.Ext(rightTex))
if ext == ".jpg" || ext == ".jpeg" {
imgDecoder = jpeg.Decode
pixelDecoder = pixelsFromNrgbaJpg
} else if ext == ".png" {
imgDecoder = png.Decode
pixelDecoder = pixelsFromNrgbaPng
} else {
return Cubemap{}, fmt.Errorf("unknown image extension: %s. Expected one of: .jpg, .jpeg, .png", ext)
}
cmap := Cubemap{
RightPath: rightTex,
LeftPath: leftTex,
TopPath: topTex,
BotPath: botTex,
FrontPath: frontTex,
BackPath: backTex,
}
gl.GenTextures(1, &cmap.TexID)
gl.BindTexture(gl.TEXTURE_CUBE_MAP, cmap.TexID)
// The order here matters
texturePaths := []string{rightTex, leftTex, topTex, botTex, frontTex, backTex}
for i := uint32(0); i < uint32(len(texturePaths)); i++ {
fPath := texturePaths[i]
//Load from disk
fileBytes, err := os.ReadFile(fPath)
if err != nil {
return Cubemap{}, err
}
img, err := imgDecoder(bytes.NewReader(fileBytes))
if err != nil {
return Cubemap{}, err
}
pixels, width, height := pixelDecoder(img)
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X)+i, 0, gl.RGBA8, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&pixels[0]))
}
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
return cmap, nil
}

View File

@ -3,7 +3,7 @@ package buffers
import (
"fmt"
"github.com/bloeys/nmage/asserts"
"github.com/bloeys/nmage/assert"
"github.com/go-gl/gl/v4.1-core/gl"
)
@ -28,6 +28,6 @@ func (b BufUsage) ToGL() uint32 {
return gl.STREAM_DRAW
}
asserts.T(false, fmt.Sprintf("Unexpected BufUsage value '%v'", b))
assert.T(false, fmt.Sprintf("Unexpected BufUsage value '%v'", b))
return 0
}

View File

@ -10,9 +10,9 @@ type Buffer struct {
//BufID is the ID of the VBO
BufID uint32
//IndexBufID is the ID of the index/element buffer
IndexBufID uint32
IndexBufCount int32
Stride int32
IndexBufID uint32
// IndexBufCount int32
Stride int32
layout []Element
}
@ -30,7 +30,28 @@ func (b *Buffer) SetData(values []float32) {
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
gl.BufferData(gl.ARRAY_BUFFER, len(values)*4, gl.Ptr(values), BufUsage_Static.ToGL())
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static.ToGL())
} else {
gl.BufferData(gl.ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func (b *Buffer) SetDataWithUsage(values []float32, usage BufUsage) {
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ARRAY_BUFFER, 0, gl.Ptr(nil), usage.ToGL())
} else {
gl.BufferData(gl.ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), usage.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
@ -38,11 +59,16 @@ func (b *Buffer) SetData(values []float32) {
func (b *Buffer) SetIndexBufData(values []uint32) {
b.IndexBufCount = int32(len(values))
// b.IndexBufCount = int32(len(values))
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, b.IndexBufID)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(values)*4, gl.Ptr(values), BufUsage_Static.ToGL())
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static.ToGL())
} else {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0)
@ -54,6 +80,8 @@ func (b *Buffer) GetLayout() []Element {
return e
}
//SetLayout updates the layout object and the corresponding vertex attributes.
//Vertex attributes are also enabled.
func (b *Buffer) SetLayout(layout ...Element) {
b.layout = layout
@ -64,6 +92,20 @@ func (b *Buffer) SetLayout(layout ...Element) {
b.layout[i].Offset = int(b.Stride)
b.Stride += b.layout[i].Size()
}
//Set opengl stuff
b.Bind()
//NOTE: VBOs are only bound at 'VertexAttribPointer', not BindBUffer, so we need to bind the buffer and vao here
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
for i := 0; i < len(layout); i++ {
gl.EnableVertexAttribArray(uint32(i))
gl.VertexAttribPointerWithOffset(uint32(i), layout[i].ElementType.CompCount(), layout[i].ElementType.GLType(), false, b.Stride, uintptr(layout[i].Offset))
}
b.UnBind()
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func NewBuffer(layout ...Element) Buffer {

View File

@ -3,7 +3,7 @@ package buffers
import (
"fmt"
"github.com/bloeys/nmage/asserts"
"github.com/bloeys/nmage/assert"
"github.com/go-gl/gl/v4.1-core/gl"
)
@ -45,7 +45,7 @@ func (dt ElementType) GLType() uint32 {
return gl.FLOAT
default:
asserts.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
@ -68,7 +68,7 @@ func (dt ElementType) CompSize() int32 {
return 4
default:
asserts.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
@ -92,7 +92,7 @@ func (dt ElementType) CompCount() int32 {
return 4
default:
asserts.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
@ -116,7 +116,7 @@ func (dt ElementType) Size() int32 {
return 4 * 4
default:
asserts.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}

100
camera/camera.go Executable file
View File

@ -0,0 +1,100 @@
package camera
import (
"github.com/bloeys/gglm/gglm"
)
type Type int32
const (
Type_Unknown Type = iota
Type_Perspective
Type_Orthographic
)
type Camera struct {
Type Type
Pos gglm.Vec3
Forward gglm.Vec3
WorldUp gglm.Vec3
NearClip float32
FarClip float32
// Perspective data
Fov float32
AspectRatio float32
// Ortho data
Left, Right, Top, Bottom float32
// Matrices
ViewMat gglm.Mat4
ProjMat gglm.Mat4
}
// Update recalculates view matrix and projection matrix.
// Should be called whenever a camera parameter changes
func (c *Camera) Update() {
c.ViewMat = gglm.LookAtRH(&c.Pos, c.Pos.Clone().Add(&c.Forward), &c.WorldUp).Mat4
if c.Type == Type_Perspective {
c.ProjMat = *gglm.Perspective(c.Fov, c.AspectRatio, c.NearClip, c.FarClip)
} else {
c.ProjMat = gglm.Ortho(c.Left, c.Right, c.Top, c.Bottom, c.NearClip, c.FarClip).Mat4
}
}
// UpdateRotation calculates a new forward vector and then calls camera.Update()
func (c *Camera) UpdateRotation(pitch, yaw float32) {
dir := gglm.NewVec3(
gglm.Cos32(yaw)*gglm.Cos32(pitch),
gglm.Sin32(pitch),
gglm.Sin32(yaw)*gglm.Cos32(pitch),
)
c.Forward = *dir.Normalize()
c.Update()
}
func NewPerspective(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, fovRadians, aspectRatio float32) *Camera {
cam := &Camera{
Type: Type_Perspective,
Pos: *pos,
Forward: *forward,
WorldUp: *worldUp,
NearClip: nearClip,
FarClip: farClip,
Fov: fovRadians,
AspectRatio: aspectRatio,
}
cam.Update()
return cam
}
func NewOrthographic(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, left, right, top, bottom float32) *Camera {
cam := &Camera{
Type: Type_Orthographic,
Pos: *pos,
Forward: *forward,
WorldUp: *worldUp,
NearClip: nearClip,
FarClip: farClip,
Left: left,
Right: right,
Top: top,
Bottom: bottom,
}
cam.Update()
return cam
}

View File

@ -1,4 +1,4 @@
//go:build debug
//go:build !release
package consts

View File

@ -1,4 +1,4 @@
//go:build !debug
//go:build release
package consts

View File

@ -1,14 +1,98 @@
package engine
import (
"runtime"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/input"
"github.com/bloeys/nmage/renderer"
"github.com/bloeys/nmage/timing"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/inkyblackness/imgui-go/v4"
"github.com/veandco/go-sdl2/sdl"
)
var (
isInited = false
)
type Window struct {
SDLWin *sdl.Window
GlCtx sdl.GLContext
SDLWin *sdl.Window
GlCtx sdl.GLContext
EventCallbacks []func(sdl.Event)
Rend renderer.Render
}
func (w *Window) handleInputs() {
input.EventLoopStart()
imIO := imgui.CurrentIO()
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
//Fire callbacks
for i := 0; i < len(w.EventCallbacks); i++ {
w.EventCallbacks[i](event)
}
//Internal processing
switch e := event.(type) {
case *sdl.MouseWheelEvent:
input.HandleMouseWheelEvent(e)
xDelta, yDelta := input.GetMouseWheelMotion()
imIO.AddMouseWheelDelta(float32(xDelta), float32(yDelta))
case *sdl.KeyboardEvent:
input.HandleKeyboardEvent(e)
if e.Type == sdl.KEYDOWN {
imIO.KeyPress(int(e.Keysym.Scancode))
} else if e.Type == sdl.KEYUP {
imIO.KeyRelease(int(e.Keysym.Scancode))
}
case *sdl.TextInputEvent:
imIO.AddInputCharacters(string(e.Text[:]))
case *sdl.MouseButtonEvent:
input.HandleMouseBtnEvent(e)
case *sdl.MouseMotionEvent:
input.HandleMouseMotionEvent(e)
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED {
w.handleWindowResize()
}
case *sdl.QuitEvent:
input.HandleQuitEvent(e)
}
}
// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
x, y, _ := sdl.GetMouseState()
imIO.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)})
imIO.SetMouseButtonDown(0, input.MouseDown(sdl.BUTTON_LEFT))
imIO.SetMouseButtonDown(1, input.MouseDown(sdl.BUTTON_RIGHT))
imIO.SetMouseButtonDown(2, input.MouseDown(sdl.BUTTON_MIDDLE))
imIO.KeyShift(sdl.SCANCODE_LSHIFT, sdl.SCANCODE_RSHIFT)
imIO.KeyCtrl(sdl.SCANCODE_LCTRL, sdl.SCANCODE_RCTRL)
imIO.KeyAlt(sdl.SCANCODE_LALT, sdl.SCANCODE_RALT)
}
func (w *Window) handleWindowResize() {
fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize()
if fbWidth <= 0 || fbHeight <= 0 {
return
}
gl.Viewport(0, 0, fbWidth, fbHeight)
}
func (w *Window) Destroy() error {
@ -17,6 +101,9 @@ func (w *Window) Destroy() error {
func Init() error {
isInited = true
runtime.LockOSThread()
timing.Init()
err := initSDL()
@ -25,7 +112,7 @@ func Init() error {
func initSDL() error {
err := sdl.Init(sdl.INIT_EVERYTHING)
err := sdl.Init(sdl.INIT_TIMER | sdl.INIT_VIDEO)
if err != nil {
return err
}
@ -49,16 +136,17 @@ func initSDL() error {
return nil
}
func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags) (*Window, error) {
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags)
func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags, rend)
}
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags) (*Window, error) {
return createWindow(title, -1, -1, width, height, WindowFlags_OPENGL|flags)
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
return createWindow(title, -1, -1, width, height, WindowFlags_OPENGL|flags, rend)
}
func createWindow(title string, x, y, width, height int32, flags WindowFlags) (*Window, error) {
func createWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
assert.T(isInited, "engine.Init was not called!")
if x == -1 && y == -1 {
x = sdl.WINDOWPOS_CENTERED
y = sdl.WINDOWPOS_CENTERED
@ -68,7 +156,11 @@ func createWindow(title string, x, y, width, height int32, flags WindowFlags) (*
if err != nil {
return nil, err
}
win := &Window{SDLWin: sdlWin}
win := &Window{
SDLWin: sdlWin,
EventCallbacks: make([]func(sdl.Event), 0),
Rend: rend,
}
win.GlCtx, err = sdlWin.GLCreateContext()
if err != nil {
@ -94,11 +186,16 @@ func initOpenGL() error {
gl.CullFace(gl.BACK)
gl.FrontFace(gl.CCW)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.ClearColor(0, 0, 0, 1)
return nil
}
func SetVSync(enabled bool) {
assert.T(isInited, "engine.Init was not called!")
if enabled {
sdl.GLSetSwapInterval(1)
} else {

68
engine/game.go Executable file
View File

@ -0,0 +1,68 @@
package engine
import (
"github.com/bloeys/nmage/timing"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/go-gl/gl/v4.1-core/gl"
)
var (
isRunning = false
)
type Game interface {
Init()
Update()
Render()
FrameEnd()
DeInit()
}
func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
isRunning = true
// Run init with an active Imgui frame to allow init full imgui access
timing.FrameStarted()
w.handleInputs()
width, height := w.SDLWin.GetSize()
ui.FrameStart(float32(width), float32(height))
g.Init()
fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize()
ui.Render(float32(width), float32(height), fbWidth, fbHeight)
timing.FrameEnded()
for isRunning {
//PERF: Cache these
width, height = w.SDLWin.GetSize()
fbWidth, fbHeight = w.SDLWin.GLGetDrawableSize()
timing.FrameStarted()
w.handleInputs()
ui.FrameStart(float32(width), float32(height))
g.Update()
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
g.Render()
ui.Render(float32(width), float32(height), fbWidth, fbHeight)
w.SDLWin.GLSwap()
g.FrameEnd()
w.Rend.FrameEnd()
timing.FrameEnded()
}
g.DeInit()
}
func Quit() {
isRunning = false
}

27
entity/base_comp.go Executable file
View File

@ -0,0 +1,27 @@
package entity
import "github.com/bloeys/nmage/assert"
var _ Comp = &BaseComp{}
type BaseComp struct {
Entity *Entity
}
func (b *BaseComp) base() {
}
func (b *BaseComp) Init(parent *Entity) {
assert.T(parent != nil, "Component was initialized with a nil parent. That is not allowed.")
b.Entity = parent
}
func (b *BaseComp) Name() string {
return "Base Component"
}
func (b *BaseComp) Update() {
}
func (b *BaseComp) Destroy() {
}

62
entity/comp.go Executable file
View File

@ -0,0 +1,62 @@
package entity
import "github.com/bloeys/nmage/assert"
type Comp interface {
// This ensures that implementors of the Comp interface
// always embed BaseComp
base()
Name() string
Init(parent *Entity)
Update()
Destroy()
}
func AddComp[T Comp](e *Entity, c T) {
assert.T(!HasComp[T](e), "Entity with id '%v' already has component of type '%T'", e.ID, c)
e.Comps = append(e.Comps, c)
c.Init(e)
}
func HasComp[T Comp](e *Entity) bool {
for i := 0; i < len(e.Comps); i++ {
_, ok := e.Comps[i].(T)
if ok {
return true
}
}
return false
}
func GetComp[T Comp](e *Entity) (out T) {
for i := 0; i < len(e.Comps); i++ {
comp, ok := e.Comps[i].(T)
if ok {
return comp
}
}
return out
}
// DestroyComp calls Destroy on the component and then removes it from the entities component list
func DestroyComp[T Comp](e *Entity) {
for i := 0; i < len(e.Comps); i++ {
comp, ok := e.Comps[i].(T)
if ok {
comp.Destroy()
e.Comps = append(e.Comps[:i], e.Comps[i+1:]...)
return
}
}
}

49
entity/entity.go Executable file
View File

@ -0,0 +1,49 @@
package entity
type EntityFlag byte
const (
EntityFlag_None EntityFlag = 0
EntityFlag_Alive EntityFlag = 1 << (iota - 1)
)
const (
GenerationShiftBits = 64 - 8
FlagsShiftBits = 64 - 16
IndexBitMask = 0x00_00_FFFF_FFFF_FFFF
)
type EntityHandle uint64
type Entity struct {
// Byte 1: Generation; Byte 2: Flags; Bytes 3-8: Index
ID EntityHandle
Comps []Comp
}
func (e *Entity) HasFlag(ef EntityFlag) bool {
return GetFlags(e.ID)&ef > 0
}
func (e *Entity) UpdateAllComps() {
for i := 0; i < len(e.Comps); i++ {
e.Comps[i].Update()
}
}
func GetGeneration(id EntityHandle) byte {
return byte(id >> GenerationShiftBits)
}
func GetFlags(id EntityHandle) EntityFlag {
return EntityFlag(id >> FlagsShiftBits)
}
func GetIndex(id EntityHandle) uint64 {
return uint64(id & IndexBitMask)
}
func NewEntityId(generation byte, flags EntityFlag, index uint64) EntityHandle {
return EntityHandle(index | (uint64(generation) << GenerationShiftBits) | (uint64(flags) << FlagsShiftBits))
}

109
entity/registry.go Executable file
View File

@ -0,0 +1,109 @@
package entity
import (
"github.com/bloeys/nmage/assert"
)
var (
// The number of slots required to be in the free list before the free list
// is used for creating new entries
FreeListUsageThreshold uint32 = 20
)
type freeListitem struct {
EntityIndex uint64
nextFree *freeListitem
}
type Registry struct {
EntityCount uint64
Entities []Entity
FreeList *freeListitem
FreeListSize uint32
}
func (r *Registry) NewEntity() *Entity {
assert.T(r.EntityCount < uint64(len(r.Entities)), "Can not add more entities to registry because it is full")
entityToUseIndex := uint64(0)
var entityToUse *Entity = nil
if r.FreeList != nil && r.FreeListSize > FreeListUsageThreshold {
entityToUseIndex = r.FreeList.EntityIndex
entityToUse = &r.Entities[entityToUseIndex]
r.FreeList = r.FreeList.nextFree
r.FreeListSize--
} else {
for i := 0; i < len(r.Entities); i++ {
e := &r.Entities[i]
if e.HasFlag(EntityFlag_Alive) {
continue
}
entityToUse = e
entityToUseIndex = uint64(i)
break
}
}
if entityToUse == nil {
panic("failed to create new entity because we did not find a free spot in the registry. Why did the assert not go off?")
}
r.EntityCount++
entityToUse.ID = NewEntityId(GetGeneration(entityToUse.ID)+1, EntityFlag_Alive, entityToUseIndex)
assert.T(entityToUse.ID != 0, "Entity ID must not be zero")
return entityToUse
}
func (r *Registry) GetEntity(id EntityHandle) *Entity {
index := GetIndex(id)
gen := GetGeneration(id)
e := &r.Entities[index]
eGen := GetGeneration(e.ID)
if gen != eGen {
return nil
}
return e
}
// FreeEntity calls Destroy on all the entities components, resets the component list, resets the entity flags, then ads this entity to the free list
func (r *Registry) FreeEntity(id EntityHandle) {
e := r.GetEntity(id)
if e == nil {
return
}
for i := 0; i < len(e.Comps); i++ {
e.Comps[i].Destroy()
}
r.EntityCount--
eIndex := GetIndex(e.ID)
e.Comps = []Comp{}
e.ID = NewEntityId(GetGeneration(e.ID), EntityFlag_None, eIndex)
r.FreeList = &freeListitem{
EntityIndex: eIndex,
nextFree: r.FreeList,
}
r.FreeListSize++
}
func NewRegistry(size uint32) *Registry {
assert.T(size > 0, "Registry size must be more than zero")
return &Registry{
Entities: make([]Entity, size),
}
}

12
go.mod
View File

@ -1,13 +1,13 @@
module github.com/bloeys/nmage
go 1.17
go 1.18
require github.com/veandco/go-sdl2 v0.4.10
require github.com/veandco/go-sdl2 v0.4.25
require github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784
require github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6
require (
github.com/bloeys/assimp-go v0.4.2
github.com/bloeys/gglm v0.3.1
github.com/inkyblackness/imgui-go/v4 v4.3.0
github.com/bloeys/assimp-go v0.4.4
github.com/bloeys/gglm v0.43.0
github.com/inkyblackness/imgui-go/v4 v4.6.0
)

20
go.sum
View File

@ -1,17 +1,17 @@
github.com/bloeys/assimp-go v0.4.2 h1:ArVK74BCFcTO/rCGj2NgZG9xtbjnJdEn5npIeJx1Z04=
github.com/bloeys/assimp-go v0.4.2/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
github.com/bloeys/gglm v0.3.1 h1:Sy9upW7SBsBfDXrSmEhid3aQ+7J7itej+upwcxOnPMQ=
github.com/bloeys/gglm v0.3.1/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
github.com/bloeys/assimp-go v0.4.4 h1:Yn5e/RpE0Oes0YMBy8O7KkwAO4R/RpgrZPJCt08dVIU=
github.com/bloeys/assimp-go v0.4.4/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
github.com/bloeys/gglm v0.43.0 h1:ZpOghR3PHfpkigTDh+FqxLsF0gN8CD6s/bWoei6LyxI=
github.com/bloeys/gglm v0.43.0/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 h1:1Zi56D0LNfvkzM+BdoxKryvUEdyWO7LP8oRT+oSYJW0=
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/inkyblackness/imgui-go/v4 v4.3.0 h1:iyAzqWXq/dG5+6ckDPhGivtrIo6AywGQMvENKzun04s=
github.com/inkyblackness/imgui-go/v4 v4.3.0/go.mod h1:g8SAGtOYUP7rYaOB2AsVKCEHmPMDmJKgt4z6d+flhb0=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/inkyblackness/imgui-go/v4 v4.6.0 h1:ShcnXEYl80+xREGBY9OpGWePA6FfJChY9Varsm+3jjE=
github.com/inkyblackness/imgui-go/v4 v4.6.0/go.mod h1:g8SAGtOYUP7rYaOB2AsVKCEHmPMDmJKgt4z6d+flhb0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/veandco/go-sdl2 v0.4.10 h1:8QoD2bhWl7SbQDflIAUYWfl9Vq+mT8/boJFAUzAScgY=
github.com/veandco/go-sdl2 v0.4.10/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/veandco/go-sdl2 v0.4.25 h1:J5ac3KKOccp/0xGJA1PaNYKPUcZm19IxhDGs8lJofPI=
github.com/veandco/go-sdl2 v0.4.25/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=

View File

@ -31,10 +31,11 @@ type mouseWheelState struct {
}
var (
keyMap = make(map[sdl.Keycode]*keyState)
mouseBtnMap = make(map[int]*mouseBtnState)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
keyMap = make(map[sdl.Keycode]*keyState)
mouseBtnMap = make(map[int]*mouseBtnState)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
quitRequested bool
)
func EventLoopStart() {
@ -55,6 +56,16 @@ func EventLoopStart() {
mouseWheel.XDelta = 0
mouseWheel.YDelta = 0
quitRequested = false
}
func HandleQuitEvent(e *sdl.QuitEvent) {
quitRequested = true
}
func IsQuitClicked() bool {
return quitRequested
}
func HandleKeyboardEvent(e *sdl.KeyboardEvent) {

20
level/level.go Executable file
View File

@ -0,0 +1,20 @@
package level
import (
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/entity"
)
type Level struct {
*entity.Registry
Name string
}
func NewLevel(name string, maxEntities uint32) *Level {
assert.T(name != "", "Level name can not be empty")
return &Level{
Name: name,
Registry: entity.NewRegistry(maxEntities),
}
}

698
main.go
View File

@ -2,64 +2,109 @@ package main
import (
"fmt"
"runtime"
"github.com/bloeys/assimp-go/asig"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assets"
"github.com/bloeys/nmage/camera"
"github.com/bloeys/nmage/engine"
"github.com/bloeys/nmage/entity"
"github.com/bloeys/nmage/input"
"github.com/bloeys/nmage/level"
"github.com/bloeys/nmage/logging"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes"
"github.com/bloeys/nmage/renderer/rend3dgl"
"github.com/bloeys/nmage/timing"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/inkyblackness/imgui-go/v4"
"github.com/veandco/go-sdl2/sdl"
)
//TODO: Tasks:
//Engine loop
//Proper rendering setup
//Flesh out the material system
//Object
//Abstract UI
//Audio
// @Todo:
// Integrate physx
// Create VAO struct independent from VBO to support multi-VBO use cases (e.g. instancing)
// Renderer batching
// Scene graph
// Separate engine loop from rendering loop? or leave it to the user?
// Abstract keys enum away from sdl
// Proper Asset loading
// Frustum culling
// Material system editor with fields automatically extracted from the shader
//Low Priority:
// Proper Asset loading
type ImguiInfo struct {
imCtx *imgui.Context
vaoID uint32
vboID uint32
indexBufID uint32
texID uint32
}
const (
camSpeed = 15
mouseSensitivity = 0.5
)
var (
winWidth int32 = 1280
winHeight int32 = 720
window *engine.Window
isRunning bool = true
window *engine.Window
pitch float32 = 0
yaw float32 = -90
cam *camera.Camera
simpleMat *materials.Material
imguiMat *materials.Material
cubeMesh *meshes.Mesh
skyboxMat *materials.Material
modelMat = gglm.NewTrMatId()
projMat = &gglm.Mat4{}
chairMesh *meshes.Mesh
cubeMesh *meshes.Mesh
skyboxMesh *meshes.Mesh
imguiInfo *ImguiInfo
cubeModelMat = gglm.NewTrMatId()
camPos = gglm.NewVec3(0, 0, -10)
camForward = gglm.NewVec3(0, 0, 1)
lightPos1 = gglm.NewVec3(-2, 0, 2)
lightColor1 = gglm.NewVec3(1, 1, 1)
debugDepthMat *materials.Material
debugDrawDepthBuffer bool
skyboxCmap assets.Cubemap
)
type OurGame struct {
Win *engine.Window
ImGUIInfo nmageimgui.ImguiInfo
}
type TransformComp struct {
entity.BaseComp
Pos *gglm.Vec3
Rot *gglm.Quat
Scale *gglm.Vec3
}
func (t *TransformComp) Name() string {
return "Transform Component"
}
func Test() {
lvl := level.NewLevel("test level", 1000)
e1 := lvl.Registry.NewEntity()
trComp := entity.GetComp[*TransformComp](e1)
fmt.Println("Get comp before adding any:", trComp)
entity.AddComp(e1, &TransformComp{
Pos: gglm.NewVec3(0, 0, 0),
Rot: gglm.NewQuatEulerXYZ(0, 0, 0),
Scale: gglm.NewVec3(0, 0, 0),
})
trComp = entity.GetComp[*TransformComp](e1)
fmt.Println("Get transform comp:", trComp)
fmt.Printf("Entity: %+v\n", e1)
fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity())
fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity())
fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity())
}
func main() {
runtime.LockOSThread()
// Test()
// return
//Init engine
err := engine.Init()
@ -68,7 +113,7 @@ func main() {
}
//Create window
window, err = engine.CreateOpenGLWindowCentered("nMage", winWidth, winHeight, engine.WindowFlags_RESIZABLE)
window, err = engine.CreateOpenGLWindowCentered("nMage", 1280, 720, engine.WindowFlags_RESIZABLE, rend3dgl.NewRend3DGL())
if err != nil {
logging.ErrLog.Fatalln("Failed to create window. Err: ", err)
}
@ -76,384 +121,257 @@ func main() {
engine.SetVSync(false)
game := &OurGame{
Win: window,
ImGUIInfo: nmageimgui.NewImGUI(),
}
window.EventCallbacks = append(window.EventCallbacks, game.handleWindowEvents)
engine.Run(game, window, game.ImGUIInfo)
}
func (g *OurGame) handleWindowEvents(e sdl.Event) {
switch e := e.(type) {
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED {
width := e.Data1
height := e.Data2
cam.AspectRatio = float32(width) / float32(height)
cam.Update()
simpleMat.SetUnifMat4("projMat", &cam.ProjMat)
debugDepthMat.SetUnifMat4("projMat", &cam.ProjMat)
}
}
}
func (g *OurGame) Init() {
var err error
//Create materials
simpleMat = materials.NewMaterial("Simple Mat", "./res/shaders/simple")
imguiMat = materials.NewMaterial("ImGUI Mat", "./res/shaders/imgui")
simpleMat = materials.NewMaterial("Simple mat", "./res/shaders/simple.glsl")
debugDepthMat = materials.NewMaterial("Debug depth mat", "./res/shaders/debug-depth.glsl")
skyboxMat = materials.NewMaterial("Skybox mat", "./res/shaders/skybox.glsl")
//Load meshes
cubeMesh, err = meshes.NewMesh("Cube", "./res/models/tex-cube.fbx", asig.PostProcess(0))
cubeMesh, err = meshes.NewMesh("Cube", "./res/models/tex-cube.fbx", 0)
if err != nil {
logging.ErrLog.Fatalln("Failed to load cube mesh. Err: ", err)
logging.ErrLog.Fatalln("Failed to load mesh. Err: ", err)
}
//Set mesh textures on material
for _, v := range cubeMesh.TextureIDs {
simpleMat.AddTextureID(v)
chairMesh, err = meshes.NewMesh("Chair", "./res/models/chair.fbx", 0)
if err != nil {
logging.ErrLog.Fatalln("Failed to load mesh. Err: ", err)
}
initImGUI()
skyboxMesh, err = meshes.NewMesh("Skybox", "./res/models/skybox-cube.obj", 0)
if err != nil {
logging.ErrLog.Fatalln("Failed to load mesh. Err: ", err)
}
//Enable vertex attributes
simpleMat.SetAttribute(cubeMesh.Buf)
//Load textures
tex, err := assets.LoadTexturePNG("./res/textures/pallete-endesga-64-1x.png", nil)
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
skyboxCmap, err = assets.LoadCubemapTextures(
"./res/textures/sb-right.jpg", "./res/textures/sb-left.jpg",
"./res/textures/sb-top.jpg", "./res/textures/sb-bottom.jpg",
"./res/textures/sb-front.jpg", "./res/textures/sb-back.jpg",
)
if err != nil {
logging.ErrLog.Fatalln("Failed to load cubemap. Err: ", err)
}
// Configure materials
simpleMat.DiffuseTex = tex.TexID
//Movement, scale and rotation
translationMat := gglm.NewTranslationMat(gglm.NewVec3(0, 0, 0))
scaleMat := gglm.NewScaleMat(gglm.NewVec3(0.25, 0.25, 0.25))
rotMat := gglm.NewRotMat(gglm.NewQuatEuler(gglm.NewVec3(0, 0, 0).AsRad()))
scaleMat := gglm.NewScaleMat(gglm.NewVec3(1, 1, 1))
rotMat := gglm.NewRotMat(gglm.NewQuatEuler(gglm.NewVec3(-90, -90, 0).AsRad()))
modelMat.Mul(translationMat.Mul(rotMat.Mul(scaleMat)))
simpleMat.SetUnifMat4("modelMat", &modelMat.Mat4)
cubeModelMat.Mul(translationMat.Mul(rotMat.Mul(scaleMat)))
// Camera
winWidth, winHeight := g.Win.SDLWin.GetSize()
cam = camera.NewPerspective(
gglm.NewVec3(0, 0, 10),
gglm.NewVec3(0, 0, -1),
gglm.NewVec3(0, 1, 0),
0.1, 200,
45*gglm.Deg2Rad,
float32(winWidth)/float32(winHeight),
)
simpleMat.SetUnifMat4("projMat", &cam.ProjMat)
debugDepthMat.SetUnifMat4("projMat", &cam.ProjMat)
//Moves objects into the cameras view
updateViewMat()
//Perspective/Depth
projMat := gglm.Perspective(45*gglm.Deg2Rad, float32(winWidth)/float32(winHeight), 0.1, 500)
simpleMat.SetUnifMat4("projMat", projMat)
//Lights
simpleMat.SetUnifVec3("lightPos1", &lightPos1)
simpleMat.SetUnifVec3("lightColor1", &lightColor1)
simpleMat.SetUnifVec3("lightPos1", lightPos1)
simpleMat.SetUnifVec3("lightColor1", lightColor1)
}
//Game loop
for isRunning {
func (g *OurGame) Update() {
timing.FrameStarted()
handleInputs()
runGameLogic()
draw()
timing.FrameEnded()
window.SDLWin.SetTitle(fmt.Sprintf("FPS: %.0f; Elapsed: %v", 1/timing.DT(), timing.ElapsedTime()))
if input.IsQuitClicked() || input.KeyClicked(sdl.K_ESCAPE) {
engine.Quit()
}
g.updateCameraLookAround()
g.updateCameraPos()
//Rotating cubes
if input.KeyDown(sdl.K_SPACE) {
cubeModelMat.Rotate(10*timing.DT()*gglm.Deg2Rad, gglm.NewVec3(1, 1, 1).Normalize())
}
if imgui.DragFloat3("Cam Pos", &cam.Pos.Data) {
updateViewMat()
}
if imgui.DragFloat3("Cam Forward", &cam.Forward.Data) {
updateViewMat()
}
if imgui.DragFloat3("Light Pos 1", &lightPos1.Data) {
simpleMat.SetUnifVec3("lightPos1", lightPos1)
}
if imgui.DragFloat3("Light Color 1", &lightColor1.Data) {
simpleMat.SetUnifVec3("lightColor1", lightColor1)
}
if input.KeyClicked(sdl.K_F4) {
fmt.Printf("Pos: %s; Forward: %s; |Forward|: %f\n", cam.Pos.String(), cam.Forward.String(), cam.Forward.Mag())
}
g.Win.SDLWin.SetTitle(fmt.Sprint("nMage (", timing.GetAvgFPS(), " fps)"))
}
func (g *OurGame) updateCameraLookAround() {
mouseX, mouseY := input.GetMouseMotion()
if (mouseX == 0 && mouseY == 0) || !input.MouseDown(sdl.BUTTON_RIGHT) {
return
}
// Yaw
yaw += float32(mouseX) * mouseSensitivity * timing.DT()
// Pitch
pitch += float32(-mouseY) * mouseSensitivity * timing.DT()
if pitch > 89.0 {
pitch = 89.0
}
if pitch < -89.0 {
pitch = -89.0
}
// Update cam forward
cam.UpdateRotation(pitch, yaw)
updateViewMat()
}
func (g *OurGame) updateCameraPos() {
update := false
var camSpeedScale float32 = 1.0
if input.KeyDown(sdl.K_LSHIFT) {
camSpeedScale = 2
}
// Forward and backward
if input.KeyDown(sdl.K_w) {
cam.Pos.Add(cam.Forward.Clone().Scale(camSpeed * camSpeedScale * timing.DT()))
update = true
} else if input.KeyDown(sdl.K_s) {
cam.Pos.Add(cam.Forward.Clone().Scale(-camSpeed * camSpeedScale * timing.DT()))
update = true
}
// Left and right
if input.KeyDown(sdl.K_d) {
cam.Pos.Add(gglm.Cross(&cam.Forward, &cam.WorldUp).Normalize().Scale(camSpeed * camSpeedScale * timing.DT()))
update = true
} else if input.KeyDown(sdl.K_a) {
cam.Pos.Add(gglm.Cross(&cam.Forward, &cam.WorldUp).Normalize().Scale(-camSpeed * camSpeedScale * timing.DT()))
update = true
}
if update {
updateViewMat()
}
}
func (g *OurGame) Render() {
matToUse := simpleMat
imgui.Checkbox("Debug depth buffer", &debugDrawDepthBuffer)
if debugDrawDepthBuffer {
matToUse = debugDepthMat
}
tempModelMatrix := cubeModelMat.Clone()
window.Rend.Draw(chairMesh, tempModelMatrix, matToUse)
rowSize := 1
for y := 0; y < rowSize; y++ {
for x := 0; x < rowSize; x++ {
tempModelMatrix.Translate(gglm.NewVec3(-6, 0, 0))
window.Rend.Draw(cubeMesh, tempModelMatrix, matToUse)
}
tempModelMatrix.Translate(gglm.NewVec3(float32(rowSize), -1, 0))
}
g.DrawSkybox()
}
func (g *OurGame) DrawSkybox() {
gl.Disable(gl.CULL_FACE)
gl.DepthFunc(gl.LEQUAL)
skyboxMesh.Buf.Bind()
skyboxMat.Bind()
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_CUBE_MAP, skyboxCmap.TexID)
viewMat := cam.ViewMat.Clone()
viewMat.Set(0, 3, 0)
viewMat.Set(1, 3, 0)
viewMat.Set(2, 3, 0)
viewMat.Set(3, 0, 0)
viewMat.Set(3, 1, 0)
viewMat.Set(3, 2, 0)
viewMat.Set(3, 3, 0)
skyboxMat.SetUnifMat4("viewMat", viewMat)
skyboxMat.SetUnifMat4("projMat", &cam.ProjMat)
// window.Rend.Draw(cubeMesh, gglm.NewTrMatId(), skyboxMat)
for i := 0; i < len(skyboxMesh.SubMeshes); i++ {
gl.DrawElementsBaseVertexWithOffset(gl.TRIANGLES, skyboxMesh.SubMeshes[i].IndexCount, gl.UNSIGNED_INT, uintptr(skyboxMesh.SubMeshes[i].BaseIndex), skyboxMesh.SubMeshes[i].BaseVertex)
}
gl.DepthFunc(gl.LESS)
gl.Enable(gl.CULL_FACE)
}
func (g *OurGame) FrameEnd() {
}
func (g *OurGame) DeInit() {
g.Win.Destroy()
}
func updateViewMat() {
targetPos := camPos.Clone().Add(camForward)
viewMat := gglm.LookAt(camPos, targetPos, gglm.NewVec3(0, 1, 0))
simpleMat.SetUnifMat4("viewMat", &viewMat.Mat4)
}
func initImGUI() {
imguiInfo = &ImguiInfo{
imCtx: imgui.CreateContext(nil),
}
imIO := imgui.CurrentIO()
imIO.SetBackendFlags(imIO.GetBackendFlags() | imgui.BackendFlagsRendererHasVtxOffset)
gl.GenVertexArrays(1, &imguiInfo.vaoID)
gl.GenBuffers(1, &imguiInfo.vboID)
gl.GenBuffers(1, &imguiInfo.indexBufID)
gl.GenTextures(1, &imguiInfo.texID)
// Upload font to gpu
gl.BindTexture(gl.TEXTURE_2D, imguiInfo.texID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
image := imIO.Fonts().TextureDataAlpha8()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels)
// Store our identifier
imIO.Fonts().SetTextureID(imgui.TextureID(imguiInfo.texID))
//Shader attributes
imguiMat.Bind()
imguiMat.EnableAttribute("Position")
imguiMat.EnableAttribute("UV")
imguiMat.EnableAttribute("Color")
imguiMat.UnBind()
//Init imgui input mapping
keys := map[int]int{
imgui.KeyTab: sdl.SCANCODE_TAB,
imgui.KeyLeftArrow: sdl.SCANCODE_LEFT,
imgui.KeyRightArrow: sdl.SCANCODE_RIGHT,
imgui.KeyUpArrow: sdl.SCANCODE_UP,
imgui.KeyDownArrow: sdl.SCANCODE_DOWN,
imgui.KeyPageUp: sdl.SCANCODE_PAGEUP,
imgui.KeyPageDown: sdl.SCANCODE_PAGEDOWN,
imgui.KeyHome: sdl.SCANCODE_HOME,
imgui.KeyEnd: sdl.SCANCODE_END,
imgui.KeyInsert: sdl.SCANCODE_INSERT,
imgui.KeyDelete: sdl.SCANCODE_DELETE,
imgui.KeyBackspace: sdl.SCANCODE_BACKSPACE,
imgui.KeySpace: sdl.SCANCODE_BACKSPACE,
imgui.KeyEnter: sdl.SCANCODE_RETURN,
imgui.KeyEscape: sdl.SCANCODE_ESCAPE,
imgui.KeyA: sdl.SCANCODE_A,
imgui.KeyC: sdl.SCANCODE_C,
imgui.KeyV: sdl.SCANCODE_V,
imgui.KeyX: sdl.SCANCODE_X,
imgui.KeyY: sdl.SCANCODE_Y,
imgui.KeyZ: sdl.SCANCODE_Z,
}
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
for imguiKey, nativeKey := range keys {
imIO.KeyMap(imguiKey, nativeKey)
}
}
func handleInputs() {
input.EventLoopStart()
imIO := imgui.CurrentIO()
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch e := event.(type) {
case *sdl.MouseWheelEvent:
input.HandleMouseWheelEvent(e)
xDelta, yDelta := input.GetMouseWheelMotion()
imIO.AddMouseWheelDelta(float32(xDelta), float32(yDelta))
case *sdl.KeyboardEvent:
input.HandleKeyboardEvent(e)
if e.Type == sdl.KEYDOWN {
imIO.KeyPress(int(e.Keysym.Scancode))
} else if e.Type == sdl.KEYUP {
imIO.KeyRelease(int(e.Keysym.Scancode))
}
case *sdl.TextInputEvent:
imIO.AddInputCharacters(string(e.Text[:]))
case *sdl.MouseButtonEvent:
input.HandleMouseBtnEvent(e)
case *sdl.MouseMotionEvent:
input.HandleMouseMotionEvent(e)
case *sdl.QuitEvent:
isRunning = false
}
}
currWinWidth, currWinHeight := window.SDLWin.GetSize()
if winWidth != currWinWidth || winHeight != currWinHeight {
handleWindowResize(currWinWidth, currWinHeight)
}
// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
x, y, _ := sdl.GetMouseState()
imIO.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)})
imIO.SetMouseButtonDown(0, input.MouseDown(sdl.BUTTON_LEFT))
imIO.SetMouseButtonDown(1, input.MouseDown(sdl.BUTTON_RIGHT))
imIO.SetMouseButtonDown(2, input.MouseDown(sdl.BUTTON_MIDDLE))
imIO.KeyShift(sdl.SCANCODE_LSHIFT, sdl.SCANCODE_RSHIFT)
imIO.KeyCtrl(sdl.SCANCODE_LCTRL, sdl.SCANCODE_RCTRL)
imIO.KeyAlt(sdl.SCANCODE_LALT, sdl.SCANCODE_RALT)
}
func handleWindowResize(newWinWidth, newWinHeight int32) {
winWidth = newWinWidth
winHeight = newWinHeight
fbWidth, fbHeight := window.SDLWin.GLGetDrawableSize()
if fbWidth <= 0 || fbHeight <= 0 {
return
}
gl.Viewport(0, 0, fbWidth, fbHeight)
projMat = gglm.Perspective(45*gglm.Deg2Rad, float32(winWidth)/float32(winHeight), 0.1, 20)
simpleMat.SetUnifMat4("projMat", projMat)
}
var time uint64 = 0
var name string = ""
var ambientColor gglm.Vec3 = *gglm.NewVec3(1, 1, 1)
var ambientColorStrength float32 = 0.1
var lightPos1 gglm.Vec3 = *gglm.NewVec3(2, 2, 0)
var lightColor1 gglm.Vec3 = *gglm.NewVec3(1, 1, 1)
func runGameLogic() {
var camSpeed float32 = 15
if input.KeyDown(sdl.K_w) {
camPos.Data[1] += camSpeed * timing.DT()
updateViewMat()
}
if input.KeyDown(sdl.K_s) {
camPos.Data[1] -= camSpeed * timing.DT()
updateViewMat()
}
if input.KeyDown(sdl.K_d) {
camPos.Data[0] += camSpeed * timing.DT()
updateViewMat()
}
if input.KeyDown(sdl.K_a) {
camPos.Data[0] -= camSpeed * timing.DT()
updateViewMat()
}
if input.GetMouseWheelYNorm() > 0 {
camPos.Data[2] += 1
updateViewMat()
} else if input.GetMouseWheelYNorm() < 0 {
camPos.Data[2] -= 1
updateViewMat()
}
modelMat.Rotate(10*timing.DT()*gglm.Deg2Rad, gglm.NewVec3(1, 1, 1).Normalize())
simpleMat.SetUnifMat4("modelMat", &modelMat.Mat4)
//ImGUI
imIO := imgui.CurrentIO()
imIO.SetDisplaySize(imgui.Vec2{X: float32(winWidth), Y: float32(winHeight)})
// Setup time step (we don't use SDL_GetTicks() because it is using millisecond resolution)
frequency := sdl.GetPerformanceFrequency()
currentTime := sdl.GetPerformanceCounter()
if time > 0 {
imIO.SetDeltaTime(float32(currentTime-time) / float32(frequency))
} else {
imIO.SetDeltaTime(1.0 / 60.0)
}
time = currentTime
imgui.NewFrame()
if imgui.SliderFloat3("Ambient Color", &ambientColor.Data, 0, 1) {
simpleMat.SetUnifVec3("ambientLightColor", &ambientColor)
}
if imgui.SliderFloat("Ambient Color Strength", &ambientColorStrength, 0, 1) {
simpleMat.SetUnifFloat32("ambientStrength", ambientColorStrength)
}
if imgui.SliderFloat3("Light Pos 1", &lightPos1.Data, -10, 10) {
simpleMat.SetUnifVec3("lightPos1", &lightPos1)
}
if imgui.SliderFloat3("Light Color 1", &lightColor1.Data, 0, 1) {
simpleMat.SetUnifVec3("lightColor1", &lightColor1)
}
imgui.Render()
}
func draw() {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
simpleMat.Bind()
cubeMesh.Buf.Bind()
tempModelMat := modelMat.Clone()
rowSize := 10
for y := 0; y < rowSize; y++ {
for x := 0; x < rowSize; x++ {
simpleMat.SetUnifMat4("modelMat", &tempModelMat.Translate(gglm.NewVec3(-1, 0, 0)).Mat4)
gl.DrawElements(gl.TRIANGLES, cubeMesh.Buf.IndexBufCount, gl.UNSIGNED_INT, gl.PtrOffset(0))
}
simpleMat.SetUnifMat4("modelMat", &tempModelMat.Translate(gglm.NewVec3(float32(rowSize), -1, 0)).Mat4)
}
simpleMat.SetUnifMat4("modelMat", &modelMat.Mat4)
drawUI()
window.SDLWin.GLSwap()
}
func drawUI() {
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
fbWidth, fbHeight := window.SDLWin.GLGetDrawableSize()
if fbWidth <= 0 || fbHeight <= 0 {
return
}
drawData := imgui.RenderedDrawData()
drawData.ScaleClipRects(imgui.Vec2{
X: float32(fbWidth) / float32(winWidth),
Y: float32(fbHeight) / float32(winHeight),
})
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
gl.Enable(gl.BLEND)
gl.BlendEquation(gl.FUNC_ADD)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DEPTH_TEST)
gl.Enable(gl.SCISSOR_TEST)
gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL)
// Setup viewport, orthographic projection matrix
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right).
// DisplayMin is typically (0,0) for single viewport apps.
imguiMat.Bind()
gl.Uniform1i(gl.GetUniformLocation(imguiMat.ShaderProg.ID, gl.Str("Texture\x00")), 0)
//PERF: only update the ortho matrix on window resize
orthoMat := gglm.Ortho(0, float32(winWidth), 0, float32(winHeight), 0, 20)
imguiMat.SetUnifMat4("ProjMtx", &orthoMat.Mat4)
gl.BindSampler(0, 0) // Rely on combined texture/sampler state.
// Recreate the VAO every time
// (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and
// we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.)
gl.BindVertexArray(imguiInfo.vaoID)
gl.BindBuffer(gl.ARRAY_BUFFER, imguiInfo.vboID)
vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout()
imguiMat.EnableAttribute("Position")
imguiMat.EnableAttribute("UV")
imguiMat.EnableAttribute("Color")
gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("Position")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetPos))
gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("UV")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetUv))
gl.VertexAttribPointerWithOffset(uint32(imguiMat.GetAttribLoc("Color")), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), uintptr(vertexOffsetCol))
indexSize := imgui.IndexBufferLayout()
drawType := gl.UNSIGNED_SHORT
if indexSize == 4 {
drawType = gl.UNSIGNED_INT
}
// Draw
for _, list := range drawData.CommandLists() {
vertexBuffer, vertexBufferSize := list.VertexBuffer()
gl.BindBuffer(gl.ARRAY_BUFFER, imguiInfo.vboID)
gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW)
indexBuffer, indexBufferSize := list.IndexBuffer()
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, imguiInfo.indexBufID)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW)
for _, cmd := range list.Commands() {
if cmd.HasUserCallback() {
cmd.CallUserCallback(list)
} else {
gl.BindTexture(gl.TEXTURE_2D, imguiInfo.texID)
clipRect := cmd.ClipRect()
gl.Scissor(int32(clipRect.X), int32(fbHeight)-int32(clipRect.W), int32(clipRect.Z-clipRect.X), int32(clipRect.W-clipRect.Y))
gl.DrawElementsBaseVertex(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), gl.PtrOffset(cmd.IndexOffset()*indexSize), int32(cmd.VertexOffset()))
}
}
}
//Reset gl state
gl.Disable(gl.BLEND)
gl.Disable(gl.SCISSOR_TEST)
gl.Enable(gl.CULL_FACE)
gl.Enable(gl.DEPTH_TEST)
cam.Update()
simpleMat.SetUnifMat4("viewMat", &cam.ViewMat)
debugDepthMat.SetUnifMat4("viewMat", &cam.ViewMat)
}

View File

@ -2,7 +2,7 @@ package materials
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/logging"
"github.com/bloeys/nmage/shaders"
"github.com/go-gl/gl/v4.1-core/gl"
@ -11,50 +11,53 @@ import (
type Material struct {
Name string
ShaderProg shaders.ShaderProgram
TexIDs []uint32
DiffuseTex uint32
UnifLocs map[string]int32
AttribLocs map[string]int32
}
func (m *Material) Bind() {
gl.UseProgram(m.ShaderProg.ID)
for i, v := range m.TexIDs {
gl.ActiveTexture(gl.TEXTURE0 + uint32(i))
gl.BindTexture(gl.TEXTURE_2D, v)
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, m.DiffuseTex)
}
func (m *Material) UnBind() {
gl.UseProgram(0)
for i := range m.TexIDs {
gl.ActiveTexture(gl.TEXTURE0 + uint32(i))
gl.BindTexture(gl.TEXTURE_2D, 0)
}
//TODO: Should we unbind textures here? Are these two lines needed?
// gl.ActiveTexture(gl.TEXTURE0)
// gl.BindTexture(gl.TEXTURE_2D, 0)
}
func (m *Material) GetAttribLoc(attribName string) int32 {
return gl.GetAttribLocation(m.ShaderProg.ID, gl.Str(attribName+"\x00"))
}
func (m *Material) SetAttribute(bufObj buffers.Buffer) {
bufObj.Bind()
//NOTE: VBOs are only bound at 'VertexAttribPointer', not BindBUffer, so we need to bind the buffer and vao here
gl.BindBuffer(gl.ARRAY_BUFFER, bufObj.BufID)
layout := bufObj.GetLayout()
for i := 0; i < len(layout); i++ {
gl.EnableVertexAttribArray(uint32(i))
gl.VertexAttribPointer(uint32(i), layout[i].ElementType.CompCount(), layout[i].ElementType.GLType(), false, bufObj.Stride, gl.PtrOffset(layout[i].Offset))
loc, ok := m.AttribLocs[attribName]
if ok {
return loc
}
bufObj.UnBind()
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
loc = gl.GetAttribLocation(m.ShaderProg.ID, gl.Str(attribName+"\x00"))
assert.T(loc != -1, "Attribute '"+attribName+"' doesn't exist on material "+m.Name)
m.AttribLocs[attribName] = loc
return loc
}
func (m *Material) AddTextureID(texID uint32) {
m.TexIDs = append(m.TexIDs, texID)
func (m *Material) GetUnifLoc(uniformName string) int32 {
loc, ok := m.UnifLocs[uniformName]
if ok {
return loc
}
loc = gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
assert.T(loc != -1, "Uniform '"+uniformName+"' doesn't exist on material "+m.Name)
m.UnifLocs[uniformName] = loc
return loc
}
func (m *Material) EnableAttribute(attribName string) {
@ -66,43 +69,35 @@ func (m *Material) DisableAttribute(attribName string) {
}
func (m *Material) SetUnifInt32(uniformName string, val int32) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform1i(m.ShaderProg.ID, loc, val)
gl.ProgramUniform1i(m.ShaderProg.ID, m.GetUnifLoc(uniformName), val)
}
func (m *Material) SetUnifFloat32(uniformName string, val float32) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform1f(m.ShaderProg.ID, loc, val)
gl.ProgramUniform1f(m.ShaderProg.ID, m.GetUnifLoc(uniformName), val)
}
func (m *Material) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform2fv(m.ShaderProg.ID, loc, 1, &vec2.Data[0])
gl.ProgramUniform2fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec2.Data[0])
}
func (m *Material) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform3fv(m.ShaderProg.ID, loc, 1, &vec3.Data[0])
gl.ProgramUniform3fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec3.Data[0])
}
func (m *Material) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform4fv(m.ShaderProg.ID, loc, 1, &vec4.Data[0])
gl.ProgramUniform4fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec4.Data[0])
}
func (m *Material) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix2fv(m.ShaderProg.ID, loc, 1, false, &mat2.Data[0][0])
gl.ProgramUniformMatrix2fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat2.Data[0][0])
}
func (m *Material) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix3fv(m.ShaderProg.ID, loc, 1, false, &mat3.Data[0][0])
gl.ProgramUniformMatrix3fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat3.Data[0][0])
}
func (m *Material) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
loc := gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix4fv(m.ShaderProg.ID, loc, 1, false, &mat4.Data[0][0])
gl.ProgramUniformMatrix4fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat4.Data[0][0])
}
func (m *Material) Delete() {
@ -111,24 +106,20 @@ func (m *Material) Delete() {
func NewMaterial(matName, shaderPath string) *Material {
shdrProg, err := shaders.NewShaderProgram()
shdrProg, err := shaders.LoadAndCompileCombinedShader(shaderPath)
if err != nil {
logging.ErrLog.Fatalln("Failed to create new shader program. Err: ", err)
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
}
vertShader, err := shaders.LoadAndCompilerShader(shaderPath+".vert.glsl", shaders.VertexShaderType)
if err != nil {
logging.ErrLog.Fatalln("Failed to load and create vertex shader. Err: ", err)
}
fragShader, err := shaders.LoadAndCompilerShader(shaderPath+".frag.glsl", shaders.FragmentShaderType)
if err != nil {
logging.ErrLog.Fatalln("Failed to load and create fragment shader. Err: ", err)
}
shdrProg.AttachShader(vertShader)
shdrProg.AttachShader(fragShader)
shdrProg.Link()
return &Material{Name: matName, ShaderProg: shdrProg}
return &Material{Name: matName, ShaderProg: shdrProg, UnifLocs: make(map[string]int32), AttribLocs: make(map[string]int32)}
}
func NewMaterialSrc(matName string, shaderSrc []byte) *Material {
shdrProg, err := shaders.LoadAndCompileCombinedShaderSrc(shaderSrc)
if err != nil {
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
}
return &Material{Name: matName, ShaderProg: shdrProg, UnifLocs: make(map[string]int32), AttribLocs: make(map[string]int32)}
}

View File

@ -6,16 +6,20 @@ import (
"github.com/bloeys/assimp-go/asig"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/asserts"
"github.com/bloeys/nmage/assets"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/logging"
)
type SubMesh struct {
BaseVertex int32
BaseIndex uint32
IndexCount int32
}
type Mesh struct {
Name string
TextureIDs []uint32
Buf buffers.Buffer
Name string
Buf buffers.Buffer
SubMeshes []SubMesh
}
func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh, error) {
@ -30,45 +34,68 @@ func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh,
return nil, errors.New("No meshes found in file: " + modelPath)
}
mesh := &Mesh{Name: name}
sceneMesh := scene.Meshes[0]
mesh.Buf = buffers.NewBuffer()
asserts.T(len(sceneMesh.TexCoords[0]) > 0, "Mesh has no UV0")
layoutToUse := []buffers.Element{{ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec2}}
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
layoutToUse = append(layoutToUse, buffers.Element{ElementType: buffers.DataTypeVec4})
mesh := &Mesh{
Name: name,
Buf: buffers.NewBuffer(),
SubMeshes: make([]SubMesh, 0, 1),
}
mesh.Buf.SetLayout(layoutToUse...)
//Load diffuse textures
mat := scene.Materials[sceneMesh.MaterialIndex]
if asig.GetMaterialTextureCount(mat, asig.TextureTypeDiffuse) > 0 {
texInfo, err := asig.GetMaterialTexture(mat, asig.TextureTypeDiffuse, 0)
if err != nil {
logging.ErrLog.Fatalf("Failed to get material texture of index 0. Err: %e\n", err)
// Initial sizes assuming one submesh that has vertex pos+normals+texCoords, and 3 indices per face
var vertexBufData []float32 = make([]float32, 0, len(scene.Meshes[0].Vertices)*3*3*2)
var indexBufData []uint32 = make([]uint32, 0, len(scene.Meshes[0].Faces)*3)
for i := 0; i < len(scene.Meshes); i++ {
sceneMesh := scene.Meshes[i]
if len(sceneMesh.TexCoords[0]) == 0 {
sceneMesh.TexCoords[0] = make([]gglm.Vec3, len(sceneMesh.Vertices))
println("Zeroing tex coords for submesh", i)
}
tex, err := assets.LoadPNG(texInfo.Path)
if err != nil {
logging.ErrLog.Fatalf("Loading PNG with path '%s' failed. Err: %e\n", texInfo.Path, err)
layoutToUse := []buffers.Element{{ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec2}}
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
layoutToUse = append(layoutToUse, buffers.Element{ElementType: buffers.DataTypeVec4})
}
mesh.TextureIDs = append(mesh.TextureIDs, tex.TexID)
if i == 0 {
mesh.Buf.SetLayout(layoutToUse...)
} else {
// @NOTE: Require that all submeshes have the same vertex buffer layout
firstSubmeshLayout := mesh.Buf.GetLayout()
assert.T(len(firstSubmeshLayout) == len(layoutToUse), fmt.Sprintf("Vertex layout of submesh %d does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, firstSubmeshLayout, layoutToUse))
for i := 0; i < len(firstSubmeshLayout); i++ {
if firstSubmeshLayout[i].ElementType != layoutToUse[i].ElementType {
panic(fmt.Sprintf("Vertex layout of submesh %d does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, firstSubmeshLayout, layoutToUse))
}
}
}
arrs := []arrToInterleave{{V3s: sceneMesh.Vertices}, {V3s: sceneMesh.Normals}, {V2s: v3sToV2s(sceneMesh.TexCoords[0])}}
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
arrs = append(arrs, arrToInterleave{V4s: sceneMesh.ColorSets[0]})
}
indices := flattenFaces(sceneMesh.Faces)
mesh.SubMeshes = append(mesh.SubMeshes, SubMesh{
// Index of the vertex to start from (e.g. if index buffer says use vertex 5, and BaseVertex=3, the vertex used will be vertex 8)
BaseVertex: int32(len(vertexBufData)*4) / mesh.Buf.Stride,
// Which index (in the index buffer) to start from
BaseIndex: uint32(len(indexBufData)),
// How many indices in this submesh
IndexCount: int32(len(indices)),
})
vertexBufData = append(vertexBufData, interleave(arrs...)...)
indexBufData = append(indexBufData, indices...)
}
var values []float32
arrs := []arrToInterleave{{V3s: sceneMesh.Vertices}, {V3s: sceneMesh.Normals}, {V2s: v3sToV2s(sceneMesh.TexCoords[0])}}
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
arrs = append(arrs, arrToInterleave{V4s: sceneMesh.ColorSets[0]})
}
values = interleave(arrs...)
mesh.Buf.SetData(values)
mesh.Buf.SetIndexBufData(flattenFaces(sceneMesh.Faces))
// fmt.Printf("!!! Vertex count: %d; Submeshes: %+v\n", len(vertexBufData)*4/int(mesh.Buf.Stride), mesh.SubMeshes)
mesh.Buf.SetData(vertexBufData)
mesh.Buf.SetIndexBufData(indexBufData)
return mesh, nil
}
@ -92,9 +119,9 @@ type arrToInterleave struct {
func (a *arrToInterleave) get(i int) []float32 {
asserts.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
asserts.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
asserts.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
if len(a.V2s) > 0 {
return a.V2s[i].Data[:]
@ -107,8 +134,8 @@ func (a *arrToInterleave) get(i int) []float32 {
func interleave(arrs ...arrToInterleave) []float32 {
asserts.T(len(arrs) > 0, "No input sent to interleave")
asserts.T(len(arrs[0].V2s) > 0 || len(arrs[0].V3s) > 0 || len(arrs[0].V4s) > 0, "Interleave arrays are empty")
assert.T(len(arrs) > 0, "No input sent to interleave")
assert.T(len(arrs[0].V2s) > 0 || len(arrs[0].V3s) > 0 || len(arrs[0].V4s) > 0, "Interleave arrays are empty")
elementCount := 0
if len(arrs[0].V2s) > 0 {
@ -123,7 +150,7 @@ func interleave(arrs ...arrToInterleave) []float32 {
totalSize := 0
for i := 0; i < len(arrs); i++ {
asserts.T(len(arrs[i].V2s) == elementCount || len(arrs[i].V3s) == elementCount || len(arrs[i].V4s) == elementCount, "Mesh vertex data given to interleave is not the same length")
assert.T(len(arrs[i].V2s) == elementCount || len(arrs[i].V3s) == elementCount || len(arrs[i].V4s) == elementCount, "Mesh vertex data given to interleave is not the same length")
if len(arrs[i].V2s) > 0 {
totalSize += len(arrs[i].V2s) * 2
@ -144,34 +171,9 @@ func interleave(arrs ...arrToInterleave) []float32 {
return out
}
func flattenVec3(vec3s []gglm.Vec3) []float32 {
floats := make([]float32, len(vec3s)*3)
for i := 0; i < len(vec3s); i++ {
floats[i*3+0] = vec3s[i].X()
floats[i*3+1] = vec3s[i].Y()
floats[i*3+2] = vec3s[i].Z()
}
return floats
}
func flattenVec4(vec4s []gglm.Vec4) []float32 {
floats := make([]float32, len(vec4s)*4)
for i := 0; i < len(vec4s); i++ {
floats[i*4+0] = vec4s[i].X()
floats[i*4+1] = vec4s[i].Y()
floats[i*4+2] = vec4s[i].Z()
floats[i*4+3] = vec4s[i].W()
}
return floats
}
func flattenFaces(faces []asig.Face) []uint32 {
asserts.T(len(faces[0].Indices) == 3, fmt.Sprintf("Face doesn't have 3 indices. Index count: %v\n", len(faces[0].Indices)))
assert.T(len(faces[0].Indices) == 3, fmt.Sprintf("Face doesn't have 3 indices. Index count: %v\n", len(faces[0].Indices)))
uints := make([]uint32, len(faces)*3)
for i := 0; i < len(faces); i++ {

44
renderer/rend3dgl/rend3dgl.go Executable file
View File

@ -0,0 +1,44 @@
package rend3dgl
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes"
"github.com/bloeys/nmage/renderer"
"github.com/go-gl/gl/v4.1-core/gl"
)
var _ renderer.Render = &Rend3DGL{}
type Rend3DGL struct {
BoundMesh *meshes.Mesh
BoundMat *materials.Material
}
func (r3d *Rend3DGL) Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material) {
if mesh != r3d.BoundMesh {
mesh.Buf.Bind()
r3d.BoundMesh = mesh
}
if mat != r3d.BoundMat {
mat.Bind()
r3d.BoundMat = mat
}
mat.SetUnifMat4("modelMat", &trMat.Mat4)
for i := 0; i < len(mesh.SubMeshes); i++ {
gl.DrawElementsBaseVertexWithOffset(gl.TRIANGLES, mesh.SubMeshes[i].IndexCount, gl.UNSIGNED_INT, uintptr(mesh.SubMeshes[i].BaseIndex), mesh.SubMeshes[i].BaseVertex)
}
}
func (r3d *Rend3DGL) FrameEnd() {
r3d.BoundMesh = nil
r3d.BoundMat = nil
}
func NewRend3DGL() *Rend3DGL {
return &Rend3DGL{}
}

12
renderer/renderer.go Executable file
View File

@ -0,0 +1,12 @@
package renderer
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes"
)
type Render interface {
Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material)
FrameEnd()
}

BIN
res/models/chair.fbx Executable file

Binary file not shown.

View File

@ -1,46 +0,0 @@
# Blender v2.92.0 OBJ File: ''
# www.blender.org
mtllib obj.mtl
o Cube
v 2.275618 1.000000 0.349413
v 3.520138 -1.000000 0.102233
v 2.275618 1.000000 0.752820
v 3.520138 -1.000000 1.000000
v 0.244520 1.000000 0.349413
v -1.000000 -1.000000 0.102233
v 0.244520 1.000000 0.752820
v -1.000000 -1.000000 1.000000
vt 0.806168 0.568832
vt 0.693832 0.681168
vt 0.693832 0.568832
vt 0.375000 1.000000
vt 0.375000 0.750000
vt 0.375000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.375000 0.500000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.806168 0.681168
vt 0.625000 0.931168
vt 0.625000 0.068832
vn 0.0000 1.0000 0.0000
vn 0.0000 0.1227 0.9924
vn -0.8490 0.5283 0.0000
vn 0.0000 -1.0000 0.0000
vn 0.8490 0.5283 0.0000
vn 0.0000 0.1227 -0.9924
usemtl Material
s off
f 5/1/1 3/2/1 1/3/1
f 3/2/2 8/4/2 4/5/2
f 8/6/3 5/7/3 6/8/3
f 2/9/4 8/10/4 6/11/4
f 1/3/5 4/5/5 2/9/5
f 5/7/6 2/9/6 6/8/6
f 5/1/1 7/12/1 3/2/1
f 3/2/2 7/13/2 8/4/2
f 8/6/3 7/14/3 5/7/3
f 2/9/4 4/5/4 8/10/4
f 1/3/5 3/2/5 4/5/5
f 5/7/6 1/3/6 2/9/6

38
res/models/skybox-cube.obj Executable file
View File

@ -0,0 +1,38 @@
# Blender v2.92.0 OBJ File: 'chair.blend'
# www.blender.org
o Cube.002_Cube.005
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.125000 0.500000
vt 0.125000 0.750000
vt 0.875000 0.500000
vt 0.875000 0.750000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
s off
f 1/1/1 2/2/1 4/3/1 3/4/1
f 3/4/2 4/3/2 8/5/2 7/6/2
f 7/6/3 8/5/3 6/7/3 5/8/3
f 5/8/4 6/7/4 2/9/4 1/10/4
f 3/11/5 7/6/5 5/8/5 1/12/5
f 8/5/6 4/13/6 2/14/6 6/7/6

View File

@ -1,3 +1,4 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
@ -23,4 +24,29 @@ void main()
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
}
}
//shader:fragment
#version 410
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
in vec3 fragPos;
out vec4 fragColor;
uniform float near = 0.1;
uniform float far = 200.0;
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // back to NDC
return (2.0 * near * far) / (far + near - z * (far - near));
}
void main()
{
float depth = LinearizeDepth(gl_FragCoord.z) / far;
fragColor = vec4(vec3(depth), 1.0);
}

View File

@ -1,13 +0,0 @@
#version 410
uniform sampler2D Texture;
in vec2 Frag_UV;
in vec4 Frag_Color;
out vec4 Out_Color;
void main()
{
Out_Color = vec4(Frag_Color.rgb, Frag_Color.a * texture(Texture, Frag_UV.st).r);
}

View File

@ -1,17 +0,0 @@
#version 410
uniform mat4 ProjMtx;
in vec2 Position;
in vec2 UV;
in vec4 Color;
out vec2 Frag_UV;
out vec4 Frag_Color;
void main()
{
Frag_UV = UV;
Frag_Color = Color;
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
}

View File

@ -1,26 +0,0 @@
#version 410
uniform float ambientStrength = 0.1;
uniform vec3 ambientLightColor = vec3(1, 1, 1);
uniform vec3 lightPos1;
uniform vec3 lightColor1;
uniform sampler2D diffTex;
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
in vec3 fragPos;
out vec4 fragColor;
void main()
{
vec3 lightDir = normalize(lightPos1 - fragPos);
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
vec4 texColor = texture(diffTex, vertUV0);
fragColor = vec4(texColor.rgb * vertColor * (finalAmbientColor + diffStrength*lightColor1) , texColor.a);
}

55
res/shaders/simple.glsl Executable file
View File

@ -0,0 +1,55 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
layout(location=1) in vec3 vertNormalIn;
layout(location=2) in vec2 vertUV0In;
layout(location=3) in vec3 vertColorIn;
out vec3 vertNormal;
out vec2 vertUV0;
out vec3 vertColor;
out vec3 fragPos;
//MVP = Model View Projection
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
void main()
{
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
vertUV0 = vertUV0In;
vertColor = vertColorIn;
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
}
//shader:fragment
#version 410
uniform float ambientStrength = 0;
uniform vec3 ambientLightColor = vec3(1, 1, 1);
uniform vec3 lightPos1;
uniform vec3 lightColor1;
uniform sampler2D diffTex;
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
in vec3 fragPos;
out vec4 fragColor;
void main()
{
vec3 lightDir = normalize(lightPos1 - fragPos);
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
vec4 texColor = texture(diffTex, vertUV0);
fragColor = vec4(texColor.rgb * vertColor * (finalAmbientColor + diffStrength*lightColor1) , texColor.a);
}

33
res/shaders/skybox.glsl Executable file
View File

@ -0,0 +1,33 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
layout(location=1) in vec3 vertNormalIn;
layout(location=2) in vec2 vertUV0In;
layout(location=3) in vec3 vertColorIn;
out vec3 vertUV0;
uniform mat4 viewMat;
uniform mat4 projMat;
void main()
{
vertUV0 = vec3(vertPosIn.x, vertPosIn.y, -vertPosIn.z);
vec4 pos = projMat * viewMat * vec4(vertPosIn, 1.0);
gl_Position = pos.xyww;
}
//shader:fragment
#version 410
in vec3 vertUV0;
out vec4 fragColor;
uniform samplerCube skybox;
void main()
{
fragColor = texture(skybox, vertUV0);
}

BIN
res/textures/Low poly planet.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

BIN
res/textures/sb-back.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

BIN
res/textures/sb-bottom.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

BIN
res/textures/sb-front.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
res/textures/sb-left.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
res/textures/sb-right.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
res/textures/sb-top.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

View File

@ -1,6 +1,7 @@
package shaders
import (
"bytes"
"errors"
"os"
"strings"
@ -28,14 +29,69 @@ func NewShaderProgram() (ShaderProgram, error) {
return ShaderProgram{ID: id}, nil
}
func LoadAndCompilerShader(shaderPath string, shaderType ShaderType) (Shader, error) {
func LoadAndCompileCombinedShader(shaderPath string) (ShaderProgram, error) {
shaderSource, err := os.ReadFile(shaderPath)
combinedSource, err := os.ReadFile(shaderPath)
if err != nil {
logging.ErrLog.Println("Failed to read shader. Err: ", err)
return Shader{}, err
return ShaderProgram{}, err
}
return LoadAndCompileCombinedShaderSrc(combinedSource)
}
func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
shaderSources := bytes.Split(shaderSrc, []byte("//shader:"))
if len(shaderSources) == 1 {
return ShaderProgram{}, errors.New("failed to read combined shader. Did not find '//shader:vertex' or '//shader:fragment'")
}
shdrProg, err := NewShaderProgram()
if err != nil {
return ShaderProgram{}, errors.New("failed to create new shader program. Err: " + err.Error())
}
loadedShdrCount := 0
for i := 0; i < len(shaderSources); i++ {
src := shaderSources[i]
//This can happen when the shader type is at the start of the file
if len(bytes.TrimSpace(src)) == 0 {
continue
}
var shdrType ShaderType
if bytes.HasPrefix(src, []byte("vertex")) {
src = src[6:]
shdrType = VertexShaderType
} else if bytes.HasPrefix(src, []byte("fragment")) {
src = src[8:]
shdrType = FragmentShaderType
} else {
return ShaderProgram{}, errors.New("unknown shader type. Must be '//shader:vertex' or '//shader:fragment'")
}
shdr, err := CompileShaderOfType(src, shdrType)
if err != nil {
return ShaderProgram{}, err
}
loadedShdrCount++
shdrProg.AttachShader(shdr)
}
if loadedShdrCount == 0 {
return ShaderProgram{}, errors.New("no valid shaders found. Please put '//shader:vertex' or '//shader:fragment' before your shaders")
}
shdrProg.Link()
return shdrProg, nil
}
func CompileShaderOfType(shaderSource []byte, shaderType ShaderType) (Shader, error) {
shaderID := gl.CreateShader(uint32(shaderType))
if shaderID == 0 {
logging.ErrLog.Println("Failed to create shader.")

View File

@ -1,11 +1,19 @@
package timing
import "time"
import (
"time"
)
var (
dt float32 = 0.01
frameStart time.Time
startTime time.Time
//fps calculator vars
dtAccum float32 = 1
lastElapsedTime uint64 = 0
framesSinceLastFPSUpdate uint = 0
avgFps float32 = 1
)
func Init() {
@ -13,18 +21,42 @@ func Init() {
}
func FrameStarted() {
frameStart = time.Now()
//fps stuff
dtAccum += dt
framesSinceLastFPSUpdate++
et := ElapsedTime()
if et > lastElapsedTime {
avgDT := dtAccum / float32(framesSinceLastFPSUpdate)
avgFps = 1 / avgDT
dtAccum = 0
framesSinceLastFPSUpdate = 0
}
lastElapsedTime = et
}
func FrameEnded() {
//Calculate new dt
dt = float32(time.Since(frameStart).Seconds())
if dt == 0 {
dt = float32(time.Microsecond.Seconds())
}
}
//DT is frame deltatime in milliseconds
//DT is frame deltatime in seconds
func DT() float32 {
return dt
}
//GetAvgFPS returns the fps averaged over 1 second
func GetAvgFPS() float32 {
return avgFps
}
//ElapsedTime is time since game start
func ElapsedTime() uint64 {
return uint64(time.Since(startTime).Seconds())

252
ui/imgui/imgui.go Executable file
View File

@ -0,0 +1,252 @@
package nmageimgui
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/timing"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/inkyblackness/imgui-go/v4"
"github.com/veandco/go-sdl2/sdl"
)
type ImguiInfo struct {
ImCtx *imgui.Context
Mat *materials.Material
VaoID uint32
VboID uint32
IndexBufID uint32
TexID uint32
}
func (i *ImguiInfo) FrameStart(winWidth, winHeight float32) {
if err := i.ImCtx.SetCurrent(); err != nil {
assert.T(false, "Setting imgui ctx as current failed. Err: "+err.Error())
}
imIO := imgui.CurrentIO()
imIO.SetDisplaySize(imgui.Vec2{X: float32(winWidth), Y: float32(winHeight)})
imIO.SetDeltaTime(timing.DT())
imgui.NewFrame()
}
func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32) {
if err := i.ImCtx.SetCurrent(); err != nil {
assert.T(false, "Setting imgui ctx as current failed. Err: "+err.Error())
}
imgui.Render()
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
if fbWidth <= 0 || fbHeight <= 0 {
return
}
drawData := imgui.RenderedDrawData()
drawData.ScaleClipRects(imgui.Vec2{
X: float32(fbWidth) / float32(winWidth),
Y: float32(fbHeight) / float32(winHeight),
})
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
gl.Enable(gl.BLEND)
gl.BlendEquation(gl.FUNC_ADD)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DEPTH_TEST)
gl.Enable(gl.SCISSOR_TEST)
gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL)
// Setup viewport, orthographic projection matrix
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right).
// DisplayMin is typically (0,0) for single viewport apps.
i.Mat.Bind()
i.Mat.SetUnifInt32("Texture", 0)
//PERF: only update the ortho matrix on window resize
orthoMat := gglm.Ortho(0, float32(winWidth), 0, float32(winHeight), 0, 20)
i.Mat.SetUnifMat4("ProjMtx", &orthoMat.Mat4)
gl.BindSampler(0, 0) // Rely on combined texture/sampler state.
// Recreate the VAO every time
// (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and
// we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.)
gl.BindVertexArray(i.VaoID)
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout()
i.Mat.EnableAttribute("Position")
i.Mat.EnableAttribute("UV")
i.Mat.EnableAttribute("Color")
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Position")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetPos))
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("UV")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetUv))
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Color")), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), uintptr(vertexOffsetCol))
indexSize := imgui.IndexBufferLayout()
drawType := gl.UNSIGNED_SHORT
if indexSize == 4 {
drawType = gl.UNSIGNED_INT
}
// Draw
for _, list := range drawData.CommandLists() {
vertexBuffer, vertexBufferSize := list.VertexBuffer()
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW)
indexBuffer, indexBufferSize := list.IndexBuffer()
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, i.IndexBufID)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW)
for _, cmd := range list.Commands() {
if cmd.HasUserCallback() {
cmd.CallUserCallback(list)
} else {
gl.BindTexture(gl.TEXTURE_2D, i.TexID)
clipRect := cmd.ClipRect()
gl.Scissor(int32(clipRect.X), int32(fbHeight)-int32(clipRect.W), int32(clipRect.Z-clipRect.X), int32(clipRect.W-clipRect.Y))
gl.DrawElementsBaseVertex(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), gl.PtrOffset(cmd.IndexOffset()*indexSize), int32(cmd.VertexOffset()))
}
}
}
//Reset gl state
gl.Disable(gl.SCISSOR_TEST)
gl.Enable(gl.CULL_FACE)
gl.Enable(gl.DEPTH_TEST)
}
func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *imgui.FontConfig, glyphRanges *imgui.GlyphRanges) imgui.Font {
fontConfigToUse := imgui.DefaultFontConfig
if fontConfig != nil {
fontConfigToUse = *fontConfig
}
glyphRangesToUse := imgui.EmptyGlyphRanges
if glyphRanges != nil {
glyphRangesToUse = *glyphRanges
}
imIO := imgui.CurrentIO()
a := imIO.Fonts()
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse)
image := a.TextureDataAlpha8()
gl.BindTexture(gl.TEXTURE_2D, i.TexID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels)
return f
}
const imguiShdrSrc = `
//shader:vertex
#version 410
uniform mat4 ProjMtx;
in vec2 Position;
in vec2 UV;
in vec4 Color;
out vec2 Frag_UV;
out vec4 Frag_Color;
void main()
{
Frag_UV = UV;
Frag_Color = Color;
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
}
//shader:fragment
#version 410
uniform sampler2D Texture;
in vec2 Frag_UV;
in vec4 Frag_Color;
out vec4 Out_Color;
void main()
{
Out_Color = vec4(Frag_Color.rgb, Frag_Color.a * texture(Texture, Frag_UV.st).r);
}
`
func NewImGUI() ImguiInfo {
imguiInfo := ImguiInfo{
ImCtx: imgui.CreateContext(nil),
Mat: materials.NewMaterialSrc("ImGUI Mat", []byte(imguiShdrSrc)),
}
imIO := imgui.CurrentIO()
imIO.SetBackendFlags(imIO.GetBackendFlags() | imgui.BackendFlagsRendererHasVtxOffset)
gl.GenVertexArrays(1, &imguiInfo.VaoID)
gl.GenBuffers(1, &imguiInfo.VboID)
gl.GenBuffers(1, &imguiInfo.IndexBufID)
gl.GenTextures(1, &imguiInfo.TexID)
// Upload font to gpu
gl.BindTexture(gl.TEXTURE_2D, imguiInfo.TexID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
image := imIO.Fonts().TextureDataAlpha8()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels)
// Store our identifier
imIO.Fonts().SetTextureID(imgui.TextureID(imguiInfo.TexID))
//Shader attributes
imguiInfo.Mat.Bind()
imguiInfo.Mat.EnableAttribute("Position")
imguiInfo.Mat.EnableAttribute("UV")
imguiInfo.Mat.EnableAttribute("Color")
imguiInfo.Mat.UnBind()
//Init imgui input mapping
keys := map[int]int{
imgui.KeyTab: sdl.SCANCODE_TAB,
imgui.KeyLeftArrow: sdl.SCANCODE_LEFT,
imgui.KeyRightArrow: sdl.SCANCODE_RIGHT,
imgui.KeyUpArrow: sdl.SCANCODE_UP,
imgui.KeyDownArrow: sdl.SCANCODE_DOWN,
imgui.KeyPageUp: sdl.SCANCODE_PAGEUP,
imgui.KeyPageDown: sdl.SCANCODE_PAGEDOWN,
imgui.KeyHome: sdl.SCANCODE_HOME,
imgui.KeyEnd: sdl.SCANCODE_END,
imgui.KeyInsert: sdl.SCANCODE_INSERT,
imgui.KeyDelete: sdl.SCANCODE_DELETE,
imgui.KeyBackspace: sdl.SCANCODE_BACKSPACE,
imgui.KeySpace: sdl.SCANCODE_BACKSPACE,
imgui.KeyEnter: sdl.SCANCODE_RETURN,
imgui.KeyEscape: sdl.SCANCODE_ESCAPE,
imgui.KeyA: sdl.SCANCODE_A,
imgui.KeyC: sdl.SCANCODE_C,
imgui.KeyV: sdl.SCANCODE_V,
imgui.KeyX: sdl.SCANCODE_X,
imgui.KeyY: sdl.SCANCODE_Y,
imgui.KeyZ: sdl.SCANCODE_Z,
}
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
for imguiKey, nativeKey := range keys {
imIO.KeyMap(imguiKey, nativeKey)
}
return imguiInfo
}