Compare commits

...

130 Commits

Author SHA1 Message Date
fbfcbaa156 Point light shadows+cubemap array fbo+cleanup 2024-04-15 10:49:18 +04:00
c4b1dd1b3d Geometry shader support+omnidirectional depth map shader+improvements 2024-04-15 05:09:07 +04:00
a5bea5a661 Cubemap depth fbo attachments 2024-04-15 04:16:44 +04:00
22ba9ca891 Fix comment 2024-04-15 03:22:25 +04:00
92855c52f9 Shader fixes 2024-04-15 03:19:40 +04:00
594d342bf0 Remove some stuff 2024-04-14 07:59:40 +04:00
bb1946b930 Simple percentage closer filtering for shadows 2024-04-14 07:58:47 +04:00
ef2b01059a Update calcshadow 2024-04-14 06:18:41 +04:00
5fa6a06079 Clamp to border for depth map+rotating cubes 2024-04-14 06:14:10 +04:00
b718611149 Improving shadows 2024-04-14 03:45:48 +04:00
be85e20024 Basic directional shadows 2024-04-14 02:50:40 +04:00
040228319e Depth fbo+renderer work+texture slots enum+optimize shaders+more 2024-04-13 23:55:52 +04:00
ddd8db3cb0 Framebuffers+unlit shader+screen quad shader 2024-04-13 08:03:17 +04:00
692167ada2 Split buffer struct into VAO+VBO+IBO structs 2024-04-13 02:59:31 +04:00
524ef068f0 Add comment 2024-04-12 23:57:24 +04:00
b060dcdbe9 Go 1.22+fix input bug 2024-04-12 23:55:21 +04:00
e22525e2ee Default to srgba textures 2024-04-12 23:38:51 +04:00
ee61373069 Blinn-phong 2024-04-12 23:28:59 +04:00
1f922b6a47 Enable stencil test 2024-04-12 23:02:27 +04:00
9d7bdc0196 Improve error messages 2024-04-12 22:40:08 +04:00
83922f1908 Spot lights 2024-04-12 21:09:14 +04:00
c00f6d97dd Multiple point lights 2024-04-12 08:38:03 +04:00
3c0f82a735 Light maps support (diffuse+specular+normal+emission)+imgui bugs 2024-04-12 03:47:30 +04:00
c058b82a92 Shader cleanup 2024-04-12 02:17:03 +04:00
908e5e96aa Specular lighting 2024-04-12 01:22:05 +04:00
c83e263476 Update todos 2024-04-12 00:23:29 +04:00
01f06cce1e Handle relative mouse mode mouse pos for imgui 2024-04-11 22:12:33 +04:00
20ed804d2a Correctly handle imgui mouse/keyboard capture 2024-04-11 22:07:38 +04:00
80ce6d60d2 Proper support for zero handles 2023-10-09 05:03:48 +04:00
c998fc26ce Avoid deprecated gl funcs+Improve imgui with srgb 2023-10-08 04:03:54 +04:00
81b515197d Properly working MSAA and SRGB :D 2023-10-08 03:20:56 +04:00
d703a5270c x8 MSAA 2023-10-07 11:28:59 +04:00
caa76c2a5e Remove test changes 2023-10-07 10:58:57 +04:00
da50d597f9 Control over srgba textures and srgba framebuffer 2023-10-07 10:58:01 +04:00
9f9744a142 Fixed now? 2023-10-07 09:55:57 +04:00
b101d54049 Flip textures before uploading to gpu 2023-10-07 09:22:32 +04:00
41b5aea185 Use sRGBA on GPU as PNG/JPEG generally uses that nowadays 2023-10-07 08:46:33 +04:00
3574318552 Fix iterator bug in Nex() 2023-10-07 01:16:10 +04:00
05ccf3e158 Handle one more iterator case 2023-10-06 08:52:27 +04:00
4f5fd50660 Fix iterator bug 2023-10-06 08:42:02 +04:00
aaea27b543 Add an iterator to the registry 2023-10-06 08:10:11 +04:00
039d09f888 Redo and simplify registry and move to own package 2023-10-06 07:28:16 +04:00
1b83d7f9a7 Change Entity->BaseEntity + Add Entity interface 2023-10-06 04:23:42 +04:00
201d9546b2 Make basecomp not use pointer receiver 2023-10-06 04:09:57 +04:00
c1d5033eb0 Separate components from entity 2023-10-06 03:52:43 +04:00
6f646540f9 Enable imgui docking + minor changes 2023-07-24 23:40:20 +04:00
a99dd304ed Complete basic imgui integration 2023-07-24 21:14:09 +04:00
4e45995ed0 Imgui key mapping 2023-07-24 20:39:45 +04:00
a735e01a77 Start transition to github.com/AllenDang/cimgui-go for imgui because the old wrapper is now depcreated. This is auto generated so has much better chance of being supported, and we get latest imgui always (including docking!) 2023-07-24 01:05:42 +04:00
abb45e4c4a Update SDL dep 2023-07-23 23:54:47 +04:00
78ea3ae747 Better DPI handling on windows (crispy text!) 2023-02-04 05:21:48 +04:00
b44b00d7e2 Re-add IndexBufCount 2023-02-04 02:00:13 +04:00
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
8e96cf7050 Reduce objects 2022-02-05 23:13:23 +04:00
56e10049e9 Textures + basic asset loading system+ uvs 2022-02-05 23:00:19 +04:00
2bfba880a9 Textured model 2022-02-05 19:27:14 +04:00
ffc9b6aa7c Fix crash 2022-02-05 19:24:55 +04:00
f49c6bc9bb Simplify interleave code 2022-01-27 10:34:51 +04:00
c989505aa7 Remove done todos 2022-01-27 09:01:16 +04:00
9ff1149191 workflow edit 2022-01-27 05:55:22 +04:00
42d99b3cc7 Badge 2022-01-27 05:51:05 +04:00
29832b9708 go build not go run 2022-01-27 05:50:27 +04:00
9a621d0669 Type in workflow 2022-01-27 05:48:11 +04:00
e893880f3b Update to assimp-go v0.4.2 2022-01-27 05:45:46 +04:00
cbe3d5111f Update readme and workflow to mention sdl2 2022-01-27 05:43:22 +04:00
3aa53852f3 Triggers 2022-01-27 05:37:58 +04:00
46483352c7 Github action 2022-01-27 05:33:37 +04:00
e38cd90a84 Cleaning naming and usage of buffers package 2022-01-26 08:48:50 +04:00
1109caef43 Support interleaved buffers 2022-01-26 08:04:50 +04:00
e1e617e4e4 Reset gl state after drawing UI in drawUI 2022-01-23 07:50:50 +04:00
6dee7b0f1d Starting work on Engine+Mesh+Material systems/packages 2022-01-22 22:23:44 +04:00
75 changed files with 4939 additions and 1065 deletions

28
.github/workflows/build-nmage.yml vendored Executable file
View File

@ -0,0 +1,28 @@
name: build-nmage
on:
create:
workflow_dispatch:
jobs:
build-nmage-macos:
runs-on: macos-12
steps:
- name: Install golang
uses: actions/setup-go@v3
with:
go-version: '>=1.22'
- 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,5 +1,7 @@
# nMage
[![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).
This project is being built with the goals being (in no particular order):
@ -17,6 +19,7 @@ To run the project you need:
* A C/C++ compiler installed and in your path
* Windows: [MingW](https://www.mingw-w64.org/downloads/#mingw-builds) or similar
* Mac/Linux: Should be installed by default, but if not try [GCC](https://gcc.gnu.org/) or [Clang](https://releases.llvm.org/download.html)
* Install SDL2 by following their [requirements](https://github.com/veandco/go-sdl2#requirements).
* Get the required [assimp-go](https://github.com/bloeys/assimp-go) DLLs/DyLibs and place them correctly by following the assimp-go [README](https://github.com/bloeys/assimp-go#using-assimp-go).
Then you can start nMage with `go run .`

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,18 +0,0 @@
package asserts
import (
"github.com/bloeys/nmage/consts"
"github.com/bloeys/nmage/logging"
)
func True(check bool, msg string) {
if consts.Debug && !check {
logging.ErrLog.Panicln(msg)
}
}
func False(check bool, msg string) {
if consts.Debug && check {
logging.ErrLog.Panicln(msg)
}
}

32
assets/assets.go Executable file
View File

@ -0,0 +1,32 @@
package assets
var (
Textures = make(map[uint32]Texture)
TexturePaths = make(map[string]uint32)
)
func AddTextureToCache(t Texture) {
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 in-mem texture with ID:", t.TexID)
TexturePaths[t.Path] = t.TexID
}
func GetTextureFromCacheID(texID uint32) (Texture, bool) {
tex, ok := Textures[texID]
return tex, ok
}
func GetTextureFromCachePath(path string) (Texture, bool) {
tex, ok := Textures[TexturePaths[path]]
return tex, ok
}

326
assets/textures.go Executable file
View File

@ -0,0 +1,326 @@
package assets
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"strings"
"unsafe"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/mandykoh/prism"
)
type ColorFormat int
const (
ColorFormat_Unknown ColorFormat = iota
ColorFormat_RGBA8
)
type Texture struct {
// Path only exists for textures loaded from disk
Path string
TexID uint32
// Width is the width of the texture in pixels (pixels per row).
// Note that the number of bytes constituting a row is MORE than this (e.g. for RGBA8, bytesPerRow=width*4, since we have 4 bytes per pixel)
Width int32
// Height is the height of the texture in pixels (pixels per column).
// Note that the number of bytes constituting a column is MORE than this (e.g. for RGBA8, bytesPerColumn=height*4, since we have 4 bytes per pixel)
Height int32
// Pixels usually stored in RGBA format
Pixels []byte
}
type TextureLoadOptions struct {
TryLoadFromCache bool
WriteToCache bool
GenMipMaps bool
KeepPixelsInMem bool
NoSrgba bool
}
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
fileBytes, err := os.ReadFile(file)
if err != nil {
return Texture{}, err
}
bytesReader := bytes.NewReader(fileBytes)
img, err := png.Decode(bytesReader)
if err != nil {
return Texture{}, err
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: file,
Pixels: nrgbaImg.Pix,
Width: int32(nrgbaImg.Bounds().Dx()),
Height: int32(nrgbaImg.Bounds().Dy()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//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
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, 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 LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: "",
Pixels: nrgbaImg.Pix,
Height: int32(nrgbaImg.Bounds().Dy()),
Width: int32(nrgbaImg.Bounds().Dx()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//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
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, 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
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: file,
Pixels: nrgbaImg.Pix,
Height: int32(nrgbaImg.Bounds().Dy()),
Width: int32(nrgbaImg.Bounds().Dx()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//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
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, 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
}
// LoadCubemapTextures only supports the 'TextureIsSrgba' option
func LoadCubemapTextures(rightTex, leftTex, topTex, botTex, frontTex, backTex string, loadOptions *TextureLoadOptions) (Cubemap, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
var imgDecoder func(r io.Reader) (image.Image, error)
ext := strings.ToLower(path.Ext(rightTex))
if ext == ".jpg" || ext == ".jpeg" {
imgDecoder = jpeg.Decode
} else if ext == ".png" {
imgDecoder = png.Decode
} 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
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
height := int32(nrgbaImg.Bounds().Dy())
width := int32(nrgbaImg.Bounds().Dx())
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X)+i, 0, internalFormat, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&nrgbaImg.Pix[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
}
func flipImgPixelsVertically(bytes []byte, width, height, bytesPerPixel int) {
// Flip the image vertically such that (e.g. in an image of 10 rows) rows 0<->9, 1<->8, 2<->7 etc are swapped.
// We do this because images are usually stored top-left to bottom-right, while opengl stores textures bottom-left to top-right, so if we don't swap
// rows textures will appear inverted
widthInBytes := width * bytesPerPixel
rowData := make([]byte, width*bytesPerPixel)
for rowNum := 0; rowNum < height/2; rowNum++ {
upperRowStartIndex := rowNum * widthInBytes
lowerRowStartIndex := (height - rowNum - 1) * widthInBytes
copy(rowData, bytes[upperRowStartIndex:upperRowStartIndex+widthInBytes])
copy(bytes[upperRowStartIndex:upperRowStartIndex+widthInBytes], bytes[lowerRowStartIndex:lowerRowStartIndex+widthInBytes])
copy(bytes[lowerRowStartIndex:lowerRowStartIndex+widthInBytes], rowData)
}
}

35
buffers/buf_usage.go Executable file
View File

@ -0,0 +1,35 @@
package buffers
import (
"fmt"
"github.com/bloeys/nmage/assert"
"github.com/go-gl/gl/v4.1-core/gl"
)
type BufUsage int
const (
BufUsage_Unknown BufUsage = iota
//Buffer is set only once and used many times
BufUsage_Static
//Buffer is changed a lot and used many times
BufUsage_Dynamic
//Buffer is set only once and used by the GPU at most a few times
BufUsage_Stream
)
func (b BufUsage) ToGL() uint32 {
switch b {
case BufUsage_Static:
return gl.STATIC_DRAW
case BufUsage_Dynamic:
return gl.DYNAMIC_DRAW
case BufUsage_Stream:
return gl.STREAM_DRAW
}
assert.T(false, fmt.Sprintf("Unexpected BufUsage value '%v'", b))
return 0
}

View File

@ -1,173 +0,0 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type BufGLType int
const (
BufGLTypeUnknown BufGLType = 0
//Generic array of data. Should be used for most data like vertex positions, vertex colors etc.
BufGLTypeArray BufGLType = gl.ARRAY_BUFFER
BufGLTypeIndices BufGLType = gl.ELEMENT_ARRAY_BUFFER
)
type BufType int
const (
BufTypeUnknown BufType = iota
BufTypeVertPos
BufTypeColor
BufTypeIndex
BufTypeNormal
)
func (bt BufType) GetBufferGLType() BufGLType {
switch bt {
case BufTypeNormal:
fallthrough
case BufTypeColor:
fallthrough
case BufTypeVertPos:
return BufGLTypeArray
case BufTypeIndex:
return BufGLTypeIndices
default:
logging.WarnLog.Println("Unknown BufferType. BufferType: ", bt)
return BufGLTypeUnknown
}
}
type BufUsage int
const (
//Buffer is set only once and used many times
BufUsageStatic BufUsage = gl.STATIC_DRAW
//Buffer is changed a lot and used many times
BufUsageDynamic BufUsage = gl.DYNAMIC_DRAW
//Buffer is set only once and used by the GPU at most a few times
BufUsageStream BufUsage = gl.STREAM_DRAW
)
type Buffer struct {
ID uint32
Type BufType
GLType BufGLType
DataTypeInfo
//DataLen is the number of elements in the uploaded to the buffer
DataLen int32
}
func (b *Buffer) Activate() {
gl.BindBuffer(uint32(b.GLType), b.ID)
}
func (b *Buffer) Deactivate() {
gl.BindBuffer(uint32(b.GLType), 0)
}
type BufferObject struct {
VAOID uint32
VertPosBuf *Buffer
NormalBuf *Buffer
ColorBuf *Buffer
IndexBuf *Buffer
}
func (bo *BufferObject) GenBuffer(data []float32, bufUsage BufUsage, bufType BufType, bufDataType DataType) {
gl.BindVertexArray(bo.VAOID)
//Create vertex buffer object
var vboID uint32
gl.GenBuffers(1, &vboID)
if vboID == 0 {
logging.ErrLog.Println("Failed to create openGL buffer")
}
buf := &Buffer{
ID: vboID,
Type: bufType,
GLType: bufType.GetBufferGLType(),
DataTypeInfo: GetDataTypeInfo(bufDataType),
DataLen: int32(len(data)),
}
bo.SetBuffer(buf)
//Fill buffer with data
gl.BindBuffer(uint32(buf.GLType), buf.ID)
gl.BufferData(uint32(buf.GLType), int(buf.DataTypeInfo.ElementSize)*len(data), gl.Ptr(data), uint32(bufUsage))
//Unbind everything
gl.BindVertexArray(0)
gl.BindBuffer(uint32(buf.GLType), 0)
}
func (bo *BufferObject) GenBufferUint32(data []uint32, bufUsage BufUsage, bufType BufType, bufDataType DataType) {
gl.BindVertexArray(bo.VAOID)
//Create vertex buffer object
var vboID uint32
gl.GenBuffers(1, &vboID)
if vboID == 0 {
logging.ErrLog.Println("Failed to create openGL buffer")
}
buf := &Buffer{
ID: vboID,
Type: bufType,
GLType: bufType.GetBufferGLType(),
DataTypeInfo: GetDataTypeInfo(bufDataType),
DataLen: int32(len(data)),
}
bo.SetBuffer(buf)
//Fill buffer with data
gl.BindBuffer(uint32(buf.GLType), buf.ID)
gl.BufferData(uint32(buf.GLType), int(buf.DataTypeInfo.ElementSize)*len(data), gl.Ptr(data), uint32(bufUsage))
//Unbind everything
gl.BindVertexArray(0)
gl.BindBuffer(uint32(buf.GLType), 0)
}
func (bo *BufferObject) SetBuffer(buf *Buffer) {
switch buf.Type {
case BufTypeVertPos:
bo.VertPosBuf = buf
case BufTypeNormal:
bo.NormalBuf = buf
case BufTypeColor:
bo.ColorBuf = buf
case BufTypeIndex:
bo.IndexBuf = buf
default:
logging.WarnLog.Println("Unknown buffer type in SetBuffer. Type:", buf.Type)
}
}
func (bo *BufferObject) Activate() {
gl.BindVertexArray(bo.VAOID)
}
func (bo *BufferObject) Deactivate() {
gl.BindVertexArray(0)
}
func NewBufferObject() *BufferObject {
var vaoID uint32
gl.GenVertexArrays(1, &vaoID)
if vaoID == 0 {
logging.ErrLog.Println("Failed to create openGL vertex array object")
}
return &BufferObject{VAOID: vaoID}
}

View File

@ -1,92 +0,0 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type DataType int
const (
DataTypeUnknown DataType = iota
DataTypeUint32
DataTypeInt32
DataTypeFloat32
DataTypeFloat64
DataTypeVec2
DataTypeVec3
DataTypeVec4
)
type DataTypeInfo struct {
//ElementSize is size in bytes of one element (e.g. for vec3 its 4)
ElementSize int32
//ElementCount is number of elements (e.g. for vec3 its 3)
ElementCount int32
//ElementType is the type of each primitive (e.g. for vec3 its gl.FLOAT)
ElementType uint32
//GLType is the type of the variable represented (e.g. for vec3 its gl.FLOAT_VEC2)
GLType uint32
}
//GetSize returns the total size in bytes (e.g. for vec3 its 4*3)
func (dti *DataTypeInfo) GetSize() int32 {
return dti.ElementSize * dti.ElementCount
}
func GetDataTypeInfo(dt DataType) DataTypeInfo {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeInt32:
return DataTypeInfo{
ElementSize: 4,
ElementCount: 1,
ElementType: gl.INT,
GLType: gl.INT,
}
case DataTypeFloat32:
return DataTypeInfo{
ElementSize: 4,
ElementCount: 1,
ElementType: gl.FLOAT,
GLType: gl.FLOAT,
}
case DataTypeFloat64:
return DataTypeInfo{
ElementSize: 8,
ElementCount: 1,
ElementType: gl.DOUBLE,
GLType: gl.DOUBLE,
}
case DataTypeVec2:
return DataTypeInfo{
ElementSize: 4,
ElementCount: 2,
ElementType: gl.FLOAT,
GLType: gl.FLOAT_VEC2,
}
case DataTypeVec3:
return DataTypeInfo{
ElementSize: 4,
ElementCount: 3,
ElementType: gl.FLOAT,
GLType: gl.FLOAT_VEC3,
}
case DataTypeVec4:
return DataTypeInfo{
ElementSize: 4,
ElementCount: 4,
ElementType: gl.FLOAT,
GLType: gl.FLOAT_VEC4,
}
default:
logging.WarnLog.Println("Unknown data type passed. DataType:", dt)
return DataTypeInfo{}
}
}

122
buffers/element.go Executable file
View File

@ -0,0 +1,122 @@
package buffers
import (
"fmt"
"github.com/bloeys/nmage/assert"
"github.com/go-gl/gl/v4.1-core/gl"
)
//Element represents an element that makes up a buffer (e.g. Vec3 at an offset of 12 bytes)
type Element struct {
Offset int
ElementType
}
//ElementType is the type of an element thats makes up a buffer (e.g. Vec3)
type ElementType int
const (
DataTypeUnknown ElementType = iota
DataTypeUint32
DataTypeInt32
DataTypeFloat32
DataTypeVec2
DataTypeVec3
DataTypeVec4
)
func (dt ElementType) GLType() uint32 {
switch dt {
case DataTypeUint32:
return gl.UNSIGNED_INT
case DataTypeInt32:
return gl.INT
case DataTypeFloat32:
fallthrough
case DataTypeVec2:
fallthrough
case DataTypeVec3:
fallthrough
case DataTypeVec4:
return gl.FLOAT
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
//CompSize returns the size in bytes for one component of the type (e.g. for Vec2 its 4)
func (dt ElementType) CompSize() int32 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
fallthrough
case DataTypeInt32:
fallthrough
case DataTypeVec2:
fallthrough
case DataTypeVec3:
fallthrough
case DataTypeVec4:
return 4
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
//CompCount returns the number of components in the element (e.g. for Vec2 its 2)
func (dt ElementType) CompCount() int32 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
fallthrough
case DataTypeInt32:
return 1
case DataTypeVec2:
return 2
case DataTypeVec3:
return 3
case DataTypeVec4:
return 4
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}
//Size returns the total size in bytes (e.g. for vec3 its 3*4=12 bytes)
func (dt ElementType) Size() int32 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
fallthrough
case DataTypeInt32:
return 4
case DataTypeVec2:
return 2 * 4
case DataTypeVec3:
return 3 * 4
case DataTypeVec4:
return 4 * 4
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
return 0
}
}

521
buffers/framebuffer.go Executable file
View File

@ -0,0 +1,521 @@
package buffers
import (
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type FramebufferAttachmentType int32
const (
FramebufferAttachmentType_Unknown FramebufferAttachmentType = iota
FramebufferAttachmentType_Texture
FramebufferAttachmentType_Renderbuffer
FramebufferAttachmentType_Cubemap
FramebufferAttachmentType_Cubemap_Array
)
func (f FramebufferAttachmentType) IsValid() bool {
switch f {
case FramebufferAttachmentType_Texture:
fallthrough
case FramebufferAttachmentType_Renderbuffer:
fallthrough
case FramebufferAttachmentType_Cubemap:
fallthrough
case FramebufferAttachmentType_Cubemap_Array:
return true
default:
return false
}
}
type FramebufferAttachmentDataFormat int32
const (
FramebufferAttachmentDataFormat_Unknown FramebufferAttachmentDataFormat = iota
FramebufferAttachmentDataFormat_R32Int
FramebufferAttachmentDataFormat_RGBA8
FramebufferAttachmentDataFormat_SRGBA
FramebufferAttachmentDataFormat_DepthF32
FramebufferAttachmentDataFormat_Depth24Stencil8
)
func (f FramebufferAttachmentDataFormat) IsColorFormat() bool {
return f == FramebufferAttachmentDataFormat_R32Int ||
f == FramebufferAttachmentDataFormat_RGBA8 ||
f == FramebufferAttachmentDataFormat_SRGBA
}
func (f FramebufferAttachmentDataFormat) IsDepthFormat() bool {
return f == FramebufferAttachmentDataFormat_Depth24Stencil8 ||
f == FramebufferAttachmentDataFormat_DepthF32
}
func (f FramebufferAttachmentDataFormat) GlInternalFormat() int32 {
switch f {
case FramebufferAttachmentDataFormat_R32Int:
return gl.R32I
case FramebufferAttachmentDataFormat_RGBA8:
return gl.RGB8
case FramebufferAttachmentDataFormat_SRGBA:
return gl.SRGB_ALPHA
case FramebufferAttachmentDataFormat_DepthF32:
return gl.DEPTH_COMPONENT
case FramebufferAttachmentDataFormat_Depth24Stencil8:
return gl.DEPTH24_STENCIL8
default:
logging.ErrLog.Fatalf("unknown framebuffer attachment data format. Format=%d\n", f)
return 0
}
}
func (f FramebufferAttachmentDataFormat) GlFormat() uint32 {
switch f {
case FramebufferAttachmentDataFormat_R32Int:
return gl.RED_INTEGER
case FramebufferAttachmentDataFormat_RGBA8:
fallthrough
case FramebufferAttachmentDataFormat_SRGBA:
return gl.RGBA
case FramebufferAttachmentDataFormat_DepthF32:
return gl.DEPTH_COMPONENT
case FramebufferAttachmentDataFormat_Depth24Stencil8:
return gl.DEPTH_STENCIL
default:
logging.ErrLog.Fatalf("unknown framebuffer attachment data format. Format=%d\n", f)
return 0
}
}
type FramebufferAttachment struct {
Id uint32
Type FramebufferAttachmentType
Format FramebufferAttachmentDataFormat
}
type Framebuffer struct {
Id uint32
ClearFlags uint32
Attachments []FramebufferAttachment
ColorAttachmentsCount uint32
Width uint32
Height uint32
}
func (fbo *Framebuffer) Bind() {
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo.Id)
}
func (fbo *Framebuffer) BindWithViewport() {
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo.Id)
gl.Viewport(0, 0, int32(fbo.Width), int32(fbo.Height))
}
// Clear calls gl.Clear with the fob's clear flags.
// Note that the fbo must be complete and bound.
// Calling this without a bound fbo will clear something else, like your screen.
func (fbo *Framebuffer) Clear() {
gl.Clear(fbo.ClearFlags)
}
func (fbo *Framebuffer) UnBind() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (fbo *Framebuffer) UnBindWithViewport(width, height uint32) {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.Viewport(0, 0, int32(width), int32(height))
}
// IsComplete returns true if OpenGL reports that the fbo is complete/usable.
// Note that this function binds and then unbinds the fbo
func (fbo *Framebuffer) IsComplete() bool {
fbo.Bind()
isComplete := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE
fbo.UnBind()
return isComplete
}
func (fbo *Framebuffer) HasColorAttachment() bool {
return fbo.ColorAttachmentsCount > 0
}
func (fbo *Framebuffer) HasDepthAttachment() bool {
for i := 0; i < len(fbo.Attachments); i++ {
a := &fbo.Attachments[i]
if a.Format.IsDepthFormat() {
return true
}
}
return false
}
func (fbo *Framebuffer) NewColorAttachment(
attachType FramebufferAttachmentType,
attachFormat FramebufferAttachmentDataFormat,
) {
if fbo.ColorAttachmentsCount == 8 {
logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due it already having %d attached\n", fbo.ColorAttachmentsCount)
}
if !attachType.IsValid() {
logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due to unknown attachment type. Type=%d\n", attachType)
}
if attachType == FramebufferAttachmentType_Cubemap || attachType == FramebufferAttachmentType_Cubemap_Array {
logging.ErrLog.Fatalf("failed creating color attachment because cubemaps can not be color attachments (at least in this implementation. You might be able to do it manually)\n")
}
if !attachFormat.IsColorFormat() {
logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due to attachment data format not being a valid color type. Data format=%d\n", attachFormat)
}
a := FramebufferAttachment{
Type: attachType,
Format: attachFormat,
}
fbo.Bind()
if attachType == FramebufferAttachmentType_Texture {
// Create texture
gl.GenTextures(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindTexture(gl.TEXTURE_2D, a.Id)
gl.TexImage2D(gl.TEXTURE_2D, 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.UNSIGNED_BYTE, nil)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Attach to fbo
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0+fbo.ColorAttachmentsCount, gl.TEXTURE_2D, a.Id, 0)
} else if attachType == FramebufferAttachmentType_Renderbuffer {
// Create rbo
gl.GenRenderbuffers(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate render buffer for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindRenderbuffer(gl.RENDERBUFFER, a.Id)
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(attachFormat.GlInternalFormat()), int32(fbo.Width), int32(fbo.Height))
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
// Attach to fbo
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0+fbo.ColorAttachmentsCount, gl.RENDERBUFFER, a.Id)
}
fbo.UnBind()
fbo.ColorAttachmentsCount++
fbo.ClearFlags |= gl.COLOR_BUFFER_BIT
fbo.Attachments = append(fbo.Attachments, a)
}
// SetNoColorBuffer sets the read and draw buffers of this fbo to 'NONE',
// which tells the graphics driver that we don't want a color buffer for this fbo.
//
// This is required because normally an fbo must have a color buffer to be considered complete, but by
// doing this we get marked as complete even without one.
//
// Usually used when you only care about some other buffer, like a depth buffer.
func (fbo *Framebuffer) SetNoColorBuffer() {
if fbo.HasColorAttachment() {
logging.ErrLog.Fatalf("failed SetNoColorBuffer because framebuffer already has a color attachment\n")
}
fbo.Bind()
gl.DrawBuffer(gl.NONE)
gl.ReadBuffer(gl.NONE)
fbo.UnBind()
}
func (fbo *Framebuffer) NewDepthAttachment(
attachType FramebufferAttachmentType,
attachFormat FramebufferAttachmentDataFormat,
) {
if fbo.HasDepthAttachment() {
logging.ErrLog.Fatalf("failed creating depth attachment for framebuffer because a depth attachment already exists\n")
}
if !attachType.IsValid() {
logging.ErrLog.Fatalf("failed creating depth attachment for framebuffer due to unknown attachment type. Type=%d\n", attachType)
}
if !attachFormat.IsDepthFormat() {
logging.ErrLog.Fatalf("failed creating depth attachment for framebuffer due to attachment data format not being a valid depth-stencil type. Data format=%d\n", attachFormat)
}
if attachType == FramebufferAttachmentType_Cubemap_Array {
logging.ErrLog.Fatalf("failed creating cubemap array depth attachment because 'NewDepthCubemapArrayAttachment' must be used for that\n")
}
a := FramebufferAttachment{
Type: attachType,
Format: attachFormat,
}
fbo.Bind()
if attachType == FramebufferAttachmentType_Texture {
// Create texture
gl.GenTextures(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindTexture(gl.TEXTURE_2D, a.Id)
gl.TexImage2D(gl.TEXTURE_2D, 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.FLOAT, nil)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
// This is so that any sampling outside the depth map gives a full depth value.
// Useful for example when doing shadow maps where we want things outside
// the range of the texture to not show shadow
borderColor := []float32{1, 1, 1, 1}
gl.TexParameterfv(gl.TEXTURE_2D, gl.TEXTURE_BORDER_COLOR, &borderColor[0])
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Attach to fbo
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, a.Id, 0)
} else if attachType == FramebufferAttachmentType_Renderbuffer {
// Create rbo
gl.GenRenderbuffers(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate render buffer for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindRenderbuffer(gl.RENDERBUFFER, a.Id)
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(attachFormat.GlInternalFormat()), int32(fbo.Width), int32(fbo.Height))
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
// Attach to fbo
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, a.Id)
} else if attachType == FramebufferAttachmentType_Cubemap {
// Create cubemap
gl.GenTextures(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindTexture(gl.TEXTURE_CUBE_MAP, a.Id)
for i := 0; i < 6; i++ {
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X+i), 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.FLOAT, nil)
}
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
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)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Attach to fbo
gl.FramebufferTexture(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, a.Id, 0)
}
fbo.UnBind()
fbo.ClearFlags |= gl.DEPTH_BUFFER_BIT
fbo.Attachments = append(fbo.Attachments, a)
}
func (fbo *Framebuffer) NewDepthCubemapArrayAttachment(
attachFormat FramebufferAttachmentDataFormat,
numCubemaps int32,
) {
if fbo.HasDepthAttachment() {
logging.ErrLog.Fatalf("failed creating cubemap array depth attachment for framebuffer because a depth attachment already exists\n")
}
if !attachFormat.IsDepthFormat() {
logging.ErrLog.Fatalf("failed creating depth attachment for framebuffer due to attachment data format not being a valid depth-stencil type. Data format=%d\n", attachFormat)
}
a := FramebufferAttachment{
Type: FramebufferAttachmentType_Cubemap_Array,
Format: attachFormat,
}
fbo.Bind()
// Create cubemap array
gl.GenTextures(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindTexture(gl.TEXTURE_CUBE_MAP_ARRAY, a.Id)
gl.TexImage3D(
gl.TEXTURE_CUBE_MAP_ARRAY,
0,
attachFormat.GlInternalFormat(),
int32(fbo.Width),
int32(fbo.Height),
6*numCubemaps,
0,
attachFormat.GlFormat(),
gl.FLOAT,
nil,
)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP_ARRAY, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Attach to fbo
gl.FramebufferTexture(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, a.Id, 0)
fbo.UnBind()
fbo.ClearFlags |= gl.DEPTH_BUFFER_BIT
fbo.Attachments = append(fbo.Attachments, a)
}
func (fbo *Framebuffer) NewDepthStencilAttachment(
attachType FramebufferAttachmentType,
attachFormat FramebufferAttachmentDataFormat,
) {
if fbo.HasDepthAttachment() {
logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer because a depth-stencil attachment already exists\n")
}
if !attachType.IsValid() {
logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer due to unknown attachment type. Type=%d\n", attachType)
}
if !attachFormat.IsDepthFormat() {
logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer due to attachment data format not being a valid depth-stencil type. Data format=%d\n", attachFormat)
}
a := FramebufferAttachment{
Type: attachType,
Format: attachFormat,
}
fbo.Bind()
if attachType == FramebufferAttachmentType_Texture {
// Create texture
gl.GenTextures(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindTexture(gl.TEXTURE_2D, a.Id)
gl.TexImage2D(gl.TEXTURE_2D, 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.UNSIGNED_INT_24_8, nil)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Attach to fbo
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, a.Id, 0)
} else if attachType == FramebufferAttachmentType_Renderbuffer {
// Create rbo
gl.GenRenderbuffers(1, &a.Id)
if a.Id == 0 {
logging.ErrLog.Fatalf("failed to generate render buffer for framebuffer. GlError=%d\n", gl.GetError())
}
gl.BindRenderbuffer(gl.RENDERBUFFER, a.Id)
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(attachFormat.GlInternalFormat()), int32(fbo.Width), int32(fbo.Height))
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
// Attach to fbo
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, a.Id)
}
fbo.UnBind()
fbo.ClearFlags |= gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT
fbo.Attachments = append(fbo.Attachments, a)
}
// SetCubemapArrayLayerFace 'binds' a single face of a cubemap from the cubemap
// array to the fbo, such that rendering only affects that one face and the others inaccessible.
//
// If this is not called, the default is that the entire cubemap array and all the faces in it
// are bound and available for use when binding the fbo.
func (fbo *Framebuffer) SetCubemapArrayLayerFace(layerFace int32) {
for i := 0; i < len(fbo.Attachments); i++ {
a := &fbo.Attachments[i]
if a.Type != FramebufferAttachmentType_Cubemap_Array {
continue
}
assert.T(a.Format.IsDepthFormat(), "SetCubemapFromArray called but a cubemap array is set on a color attachment, which is not currently handled. Code must be updated!")
gl.FramebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, a.Id, 0, layerFace)
return
}
logging.ErrLog.Fatalf("SetCubemapFromArray failed because no cubemap array attachment was found on fbo. Fbo=%+v\n", *fbo)
}
func (fbo *Framebuffer) Delete() {
if fbo.Id == 0 {
return
}
gl.DeleteFramebuffers(1, &fbo.Id)
fbo.Id = 0
}
func NewFramebuffer(width, height uint32) Framebuffer {
// It is allowed to have attachments of differnt sizes in one FBO,
// but that complicates things (e.g. which size to use for gl.viewport) and I don't see much use
// for it now, so we will have all attachments share size
fbo := Framebuffer{
Width: width,
Height: height,
}
gl.GenFramebuffers(1, &fbo.Id)
if fbo.Id == 0 {
logging.ErrLog.Fatalf("failed to generate framebuffer. GlError=%d\n", gl.GetError())
}
return fbo
}

46
buffers/index_buffer.go Executable file
View File

@ -0,0 +1,46 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type IndexBuffer struct {
Id uint32
// IndexBufCount is the number of elements in the index buffer. Updated in IndexBuffer.SetData
IndexBufCount int32
}
func (ib *IndexBuffer) Bind() {
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib.Id)
}
func (ib *IndexBuffer) UnBind() {
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0)
}
func (ib *IndexBuffer) SetData(values []uint32) {
ib.Bind()
sizeInBytes := len(values) * 4
ib.IndexBufCount = int32(len(values))
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())
}
}
func NewIndexBuffer() IndexBuffer {
ib := IndexBuffer{}
gl.GenBuffers(1, &ib.Id)
if ib.Id == 0 {
logging.ErrLog.Println("Failed to create OpenGL buffer")
}
return ib
}

54
buffers/vertex_array.go Executable file
View File

@ -0,0 +1,54 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type VertexArray struct {
Id uint32
Vbos []VertexBuffer
IndexBuffer IndexBuffer
}
func (va *VertexArray) Bind() {
gl.BindVertexArray(va.Id)
}
func (va *VertexArray) UnBind() {
gl.BindVertexArray(0)
}
func (va *VertexArray) AddVertexBuffer(vbo VertexBuffer) {
// NOTE: VBOs are only bound at 'VertexAttribPointer' (and related) calls
va.Bind()
vbo.Bind()
for i := 0; i < len(vbo.layout); i++ {
l := &vbo.layout[i]
gl.EnableVertexAttribArray(uint32(i))
gl.VertexAttribPointerWithOffset(uint32(i), l.ElementType.CompCount(), l.ElementType.GLType(), false, vbo.Stride, uintptr(l.Offset))
}
}
func (va *VertexArray) SetIndexBuffer(ib IndexBuffer) {
va.Bind()
ib.Bind()
va.IndexBuffer = ib
}
func NewVertexArray() VertexArray {
vao := VertexArray{}
gl.GenVertexArrays(1, &vao.Id)
if vao.Id == 0 {
logging.ErrLog.Println("Failed to create OpenGL vertex array object")
}
return vao
}

63
buffers/vertex_buffer.go Executable file
View File

@ -0,0 +1,63 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type VertexBuffer struct {
Id uint32
Stride int32
layout []Element
}
func (vb *VertexBuffer) Bind() {
gl.BindBuffer(gl.ARRAY_BUFFER, vb.Id)
}
func (vb *VertexBuffer) UnBind() {
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func (vb *VertexBuffer) SetData(values []float32, usage BufUsage) {
vb.Bind()
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())
}
}
func (vb *VertexBuffer) GetLayout() []Element {
e := make([]Element, len(vb.layout))
copy(e, vb.layout)
return e
}
func (vb *VertexBuffer) SetLayout(layout ...Element) {
vb.Stride = 0
vb.layout = layout
for i := 0; i < len(vb.layout); i++ {
vb.layout[i].Offset = int(vb.Stride)
vb.Stride += vb.layout[i].Size()
}
}
func NewVertexBuffer(layout ...Element) VertexBuffer {
vb := VertexBuffer{}
gl.GenBuffers(1, &vb.Id)
if vb.Id == 0 {
logging.ErrLog.Println("Failed to create OpenGL buffer")
}
vb.SetLayout(layout...)
return vb
}

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

291
engine/engine.go Executable file
View File

@ -0,0 +1,291 @@
package engine
import (
"runtime"
imgui "github.com/AllenDang/cimgui-go"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/input"
"github.com/bloeys/nmage/renderer"
"github.com/bloeys/nmage/timing"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/veandco/go-sdl2/sdl"
)
var (
isInited = false
isSdlButtonLeftDown = false
isSdlButtonMiddleDown = false
isSdlButtonRightDown = false
ImguiRelativeMouseModePosX float32
ImguiRelativeMouseModePosY float32
)
type Window struct {
SDLWin *sdl.Window
GlCtx sdl.GLContext
EventCallbacks []func(sdl.Event)
Rend renderer.Render
}
func (w *Window) handleInputs() {
input.EventLoopStart()
imIo := imgui.CurrentIO()
imguiCaptureMouse := imIo.WantCaptureMouse()
imguiCaptureKeyboard := imIo.WantCaptureKeyboard()
// These two are to fix a bug where state isn't cleared
// even after imgui captures the keyboard/mouse.
//
// For example, if player is moving due to key held and then imgui captures the keyboard,
// the player keeps moving even when the key is no longer pressed because the input system never
// receives the key up event.
if imguiCaptureMouse {
input.ClearMouseState()
}
if imguiCaptureKeyboard {
input.ClearKeyboardState()
}
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:
if !imguiCaptureMouse {
input.HandleMouseWheelEvent(e)
}
imIo.AddMouseWheelDelta(float32(e.X), float32(e.Y))
case *sdl.KeyboardEvent:
if !imguiCaptureKeyboard {
input.HandleKeyboardEvent(e)
}
imIo.AddKeyEvent(nmageimgui.SdlScancodeToImGuiKey(e.Keysym.Scancode), e.Type == sdl.KEYDOWN)
// Send modifier key updates to imgui
if e.Keysym.Sym == sdl.K_LCTRL || e.Keysym.Sym == sdl.K_RCTRL {
imIo.SetKeyCtrl(e.Type == sdl.KEYDOWN)
}
if e.Keysym.Sym == sdl.K_LSHIFT || e.Keysym.Sym == sdl.K_RSHIFT {
imIo.SetKeyShift(e.Type == sdl.KEYDOWN)
}
if e.Keysym.Sym == sdl.K_LALT || e.Keysym.Sym == sdl.K_RALT {
imIo.SetKeyAlt(e.Type == sdl.KEYDOWN)
}
if e.Keysym.Sym == sdl.K_LGUI || e.Keysym.Sym == sdl.K_RGUI {
imIo.SetKeySuper(e.Type == sdl.KEYDOWN)
}
case *sdl.TextInputEvent:
imIo.AddInputCharactersUTF8(e.GetText())
case *sdl.MouseButtonEvent:
if !imguiCaptureMouse {
input.HandleMouseBtnEvent(e)
}
isPressed := e.State == sdl.PRESSED
if e.Button == sdl.BUTTON_LEFT {
isSdlButtonLeftDown = isPressed
} else if e.Button == sdl.BUTTON_MIDDLE {
isSdlButtonMiddleDown = isPressed
} else if e.Button == sdl.BUTTON_RIGHT {
isSdlButtonRightDown = isPressed
}
case *sdl.MouseMotionEvent:
if !imguiCaptureMouse {
input.HandleMouseMotionEvent(e)
}
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED {
w.handleWindowResize()
}
case *sdl.QuitEvent:
input.HandleQuitEvent(e)
}
}
if sdl.GetRelativeMouseMode() {
imIo.SetMousePos(imgui.Vec2{X: ImguiRelativeMouseModePosX, Y: ImguiRelativeMouseModePosY})
} else {
x, y, _ := sdl.GetMouseState()
imIo.SetMousePos(imgui.Vec2{X: float32(x), Y: float32(y)})
}
// 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.
imIo.SetMouseButtonDown(imgui.MouseButtonLeft, isSdlButtonLeftDown)
imIo.SetMouseButtonDown(imgui.MouseButtonRight, isSdlButtonRightDown)
imIo.SetMouseButtonDown(imgui.MouseButtonMiddle, isSdlButtonMiddleDown)
}
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 {
return w.SDLWin.Destroy()
}
func Init() error {
isInited = true
runtime.LockOSThread()
timing.Init()
err := initSDL()
return err
}
func initSDL() error {
err := sdl.Init(sdl.INIT_TIMER | sdl.INIT_VIDEO)
if err != nil {
return err
}
sdl.ShowCursor(1)
sdl.GLSetAttribute(sdl.MAJOR_VERSION, 4)
sdl.GLSetAttribute(sdl.MINOR_VERSION, 1)
sdl.GLSetAttribute(sdl.GL_RED_SIZE, 8)
sdl.GLSetAttribute(sdl.GL_GREEN_SIZE, 8)
sdl.GLSetAttribute(sdl.GL_BLUE_SIZE, 8)
sdl.GLSetAttribute(sdl.GL_ALPHA_SIZE, 8)
sdl.GLSetAttribute(sdl.GL_DOUBLEBUFFER, 1)
sdl.GLSetAttribute(sdl.GL_DEPTH_SIZE, 24)
sdl.GLSetAttribute(sdl.GL_STENCIL_SIZE, 8)
sdl.GLSetAttribute(sdl.GL_FRAMEBUFFER_SRGB_CAPABLE, 1)
// Allows us to do MSAA
sdl.GLSetAttribute(sdl.GL_MULTISAMPLEBUFFERS, 1)
sdl.GLSetAttribute(sdl.GL_MULTISAMPLESAMPLES, 4)
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
return nil
}
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, rend renderer.Render) (*Window, error) {
return createWindow(title, sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, width, height, WindowFlags_OPENGL|flags, rend)
}
func createWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
assert.T(isInited, "engine.Init() was not called!")
sdlWin, err := sdl.CreateWindow(title, x, y, width, height, uint32(flags))
if err != nil {
return nil, err
}
win := &Window{
SDLWin: sdlWin,
EventCallbacks: make([]func(sdl.Event), 0),
Rend: rend,
}
win.GlCtx, err = sdlWin.GLCreateContext()
if err != nil {
return nil, err
}
err = initOpenGL()
if err != nil {
return nil, err
}
// Get rid of the blinding white startup screen (unfortunately there is still one frame of white)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
sdlWin.GLSwap()
return win, err
}
func initOpenGL() error {
if err := gl.Init(); err != nil {
return err
}
gl.Enable(gl.DEPTH_TEST)
gl.Enable(gl.STENCIL_TEST)
gl.Enable(gl.CULL_FACE)
gl.CullFace(gl.BACK)
gl.FrontFace(gl.CCW)
gl.Enable(gl.BLEND)
gl.Enable(gl.MULTISAMPLE)
gl.Enable(gl.FRAMEBUFFER_SRGB)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.ClearColor(0, 0, 0, 1)
return nil
}
func SetSrgbFramebuffer(isEnabled bool) {
if isEnabled {
gl.Enable(gl.FRAMEBUFFER_SRGB)
} else {
gl.Disable(gl.FRAMEBUFFER_SRGB)
}
}
func SetVSync(enabled bool) {
if enabled {
sdl.GLSetSwapInterval(1)
} else {
sdl.GLSetSwapInterval(0)
}
}
func SetMSAA(isEnabled bool) {
if isEnabled {
gl.Enable(gl.MULTISAMPLE)
} else {
gl.Disable(gl.MULTISAMPLE)
}
}

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
}

29
engine/windowflags.go Executable file
View File

@ -0,0 +1,29 @@
package engine
import "github.com/veandco/go-sdl2/sdl"
type WindowFlags int
const (
WindowFlags_FULLSCREEN WindowFlags = sdl.WINDOW_FULLSCREEN
WindowFlags_OPENGL WindowFlags = sdl.WINDOW_OPENGL
WindowFlags_SHOWN WindowFlags = sdl.WINDOW_SHOWN
WindowFlags_HIDDEN WindowFlags = sdl.WINDOW_HIDDEN
WindowFlags_BORDERLESS WindowFlags = sdl.WINDOW_BORDERLESS
WindowFlags_RESIZABLE WindowFlags = sdl.WINDOW_RESIZABLE
WindowFlags_MINIMIZED WindowFlags = sdl.WINDOW_MINIMIZED
WindowFlags_MAXIMIZED WindowFlags = sdl.WINDOW_MAXIMIZED
WindowFlags_INPUT_GRABBED WindowFlags = sdl.WINDOW_INPUT_GRABBED
WindowFlags_INPUT_FOCUS WindowFlags = sdl.WINDOW_INPUT_FOCUS
WindowFlags_MOUSE_FOCUS WindowFlags = sdl.WINDOW_MOUSE_FOCUS
WindowFlags_FULLSCREEN_DESKTOP WindowFlags = sdl.WINDOW_FULLSCREEN_DESKTOP
WindowFlags_FOREIGN WindowFlags = sdl.WINDOW_FOREIGN
WindowFlags_ALLOW_HIGHDPI WindowFlags = sdl.WINDOW_ALLOW_HIGHDPI
WindowFlags_MOUSE_CAPTURE WindowFlags = sdl.WINDOW_MOUSE_CAPTURE
WindowFlags_ALWAYS_ON_TOP WindowFlags = sdl.WINDOW_ALWAYS_ON_TOP
WindowFlags_SKIP_TASKBAR WindowFlags = sdl.WINDOW_SKIP_TASKBAR
WindowFlags_UTILITY WindowFlags = sdl.WINDOW_UTILITY
WindowFlags_TOOLTIP WindowFlags = sdl.WINDOW_TOOLTIP
WindowFlags_POPUP_MENU WindowFlags = sdl.WINDOW_POPUP_MENU
// WindowFlags_VULKAN WindowFlags = sdl.WINDOW_VULKAN
)

26
entity/base_comp.go Executable file
View File

@ -0,0 +1,26 @@
package entity
import "github.com/bloeys/nmage/registry"
var _ Comp = &BaseComp{}
type BaseComp struct {
Handle registry.Handle
}
func (b BaseComp) baseComp() {
}
func (b *BaseComp) Init(parentHandle registry.Handle) {
b.Handle = parentHandle
}
func (b BaseComp) Name() string {
return "Base Component"
}
func (b BaseComp) Update() {
}
func (b BaseComp) Destroy() {
}

73
entity/comp.go Executable file
View File

@ -0,0 +1,73 @@
package entity
import (
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/registry"
)
type Comp interface {
// This ensures that implementors of the Comp interface
// always embed BaseComp
baseComp()
Name() string
Init(parentHandle registry.Handle)
Update()
Destroy()
}
func NewCompContainer() CompContainer {
return CompContainer{Comps: []Comp{}}
}
type CompContainer struct {
Comps []Comp
}
func AddComp[T Comp](entityHandle registry.Handle, cc *CompContainer, c T) {
assert.T(!HasComp[T](cc), "Entity with id '%v' already has component of type '%T'", entityHandle, c)
cc.Comps = append(cc.Comps, c)
c.Init(entityHandle)
}
func HasComp[T Comp](e *CompContainer) 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 *CompContainer) (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 *CompContainer) {
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
}
}
}

7
entity/entity.go Executable file
View File

@ -0,0 +1,7 @@
package entity
import "github.com/bloeys/nmage/registry"
type Entity interface {
GetHandle() registry.Handle
}

18
go.mod
View File

@ -1,13 +1,19 @@
module github.com/bloeys/nmage
go 1.17
go 1.22
require github.com/veandco/go-sdl2 v0.4.10
require github.com/veandco/go-sdl2 v0.4.35
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.3.3
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
)
require (
github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2
github.com/mandykoh/prism v0.35.1
)
require github.com/mandykoh/go-parallel v0.1.0 // indirect

61
go.sum
View File

@ -1,21 +1,40 @@
github.com/bloeys/assimp-go v0.3.1 h1:GANPXH8ER/4B/XsxZOw03GLZi2qKiWapSJ9dntrGoic=
github.com/bloeys/assimp-go v0.3.1/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
github.com/bloeys/assimp-go v0.3.2 h1:CsKnLloWZyn6uYNNaQE2Jq2Q+yH4d71A+CxbpU2flng=
github.com/bloeys/assimp-go v0.3.2/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
github.com/bloeys/assimp-go v0.3.3 h1:36Cqdsv/vVWg7mx6Kvu++1Z0SftdAQF4a+ApllpVT4M=
github.com/bloeys/assimp-go v0.3.3/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/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/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/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2 h1:3HA/5qD8Rimxz/y1sLyVaM7ws1dzjXzMt4hOBiwHggo=
github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2/go.mod h1:iNfbIyOBN8k3XScMxULbrwYbPsXEAUD0Jb6UwrspQb8=
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/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/mandykoh/go-parallel v0.1.0 h1:7vJMNMC4dsbgZdkAb2A8tV5ENY1v7VxIO1wzQWZoT8k=
github.com/mandykoh/go-parallel v0.1.0/go.mod h1:lkYHqG1JNTaSS6lG+PgFCnyMd2VDy8pH9jN9pY899ig=
github.com/mandykoh/prism v0.35.1 h1:JbQfQarANxSWlgJEpjv+E7DvtrqBaVP1YgJfZPvo6ME=
github.com/mandykoh/prism v0.35.1/go.mod h1:3miB3EAJ0IggYl/4eBB5MmawRbyJI1gKDtbrVvk8Q9I=
github.com/veandco/go-sdl2 v0.4.35 h1:NohzsfageDWGtCd9nf7Pc3sokMK/MOK+UA2QMJARWzQ=
github.com/veandco/go-sdl2 v0.4.35/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -31,23 +31,26 @@ 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() {
for _, v := range keyMap {
for k, v := range keyMap {
v.IsPressedThisFrame = false
v.IsReleasedThisFrame = false
keyMap[k] = v
}
for _, v := range mouseBtnMap {
for k, v := range mouseBtnMap {
v.IsPressedThisFrame = false
v.IsReleasedThisFrame = false
v.IsDoubleClicked = false
mouseBtnMap[k] = v
}
mouseMotion.XDelta = 0
@ -55,33 +58,55 @@ func EventLoopStart() {
mouseWheel.XDelta = 0
mouseWheel.YDelta = 0
quitRequested = false
}
func ClearKeyboardState() {
clear(keyMap)
}
func ClearMouseState() {
clear(mouseBtnMap)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
}
func HandleQuitEvent(e *sdl.QuitEvent) {
quitRequested = true
}
func IsQuitClicked() bool {
return quitRequested
}
func HandleKeyboardEvent(e *sdl.KeyboardEvent) {
ks := keyMap[e.Keysym.Sym]
if ks == nil {
ks = &keyState{Key: e.Keysym.Sym}
keyMap[ks.Key] = ks
ks, ok := keyMap[e.Keysym.Sym]
if !ok {
ks = keyState{Key: e.Keysym.Sym}
}
ks.State = int(e.State)
ks.IsPressedThisFrame = e.State == sdl.PRESSED && e.Repeat == 0
ks.IsReleasedThisFrame = e.State == sdl.RELEASED && e.Repeat == 0
keyMap[ks.Key] = ks
}
func HandleMouseBtnEvent(e *sdl.MouseButtonEvent) {
mb := mouseBtnMap[int(e.Button)]
if mb == nil {
mb = &mouseBtnState{Btn: int(e.Button)}
mouseBtnMap[int(e.Button)] = mb
mb, ok := mouseBtnMap[int(e.Button)]
if !ok {
mb = mouseBtnState{Btn: int(e.Button)}
}
mb.State = int(e.State)
mb.IsDoubleClicked = e.Clicks == 2 && e.State == sdl.PRESSED
mb.IsPressedThisFrame = e.State == sdl.PRESSED
mb.IsReleasedThisFrame = e.State == sdl.RELEASED
mouseBtnMap[int(e.Button)] = mb
}
func HandleMouseMotionEvent(e *sdl.MouseMotionEvent) {
@ -98,12 +123,12 @@ func HandleMouseWheelEvent(e *sdl.MouseWheelEvent) {
mouseWheel.YDelta = e.Y
}
//GetMousePos returns the window coordinates of the mouse
// GetMousePos returns the window coordinates of the mouse
func GetMousePos() (x, y int32) {
return mouseMotion.XPos, mouseMotion.YPos
}
//GetMouseMotion returns how many pixels were moved last frame
// GetMouseMotion returns how many pixels were moved last frame
func GetMouseMotion() (xDelta, yDelta int32) {
return mouseMotion.XDelta, mouseMotion.YDelta
}
@ -130,7 +155,7 @@ func GetMouseWheelMotion() (xDelta, yDelta int32) {
return mouseWheel.XDelta, mouseWheel.YDelta
}
//GetMouseWheelXNorm returns 1 if mouse wheel xDelta > 0, -1 if xDelta < 0, and 0 otherwise
// GetMouseWheelXNorm returns 1 if mouse wheel xDelta > 0, -1 if xDelta < 0, and 0 otherwise
func GetMouseWheelXNorm() int32 {
if mouseWheel.XDelta > 0 {
@ -142,7 +167,7 @@ func GetMouseWheelXNorm() int32 {
return 0
}
//returns 1 if mouse wheel yDelta > 0, -1 if yDelta < 0, and 0 otherwise
// returns 1 if mouse wheel yDelta > 0, -1 if yDelta < 0, and 0 otherwise
func GetMouseWheelYNorm() int32 {
if mouseWheel.YDelta > 0 {
@ -156,8 +181,8 @@ func GetMouseWheelYNorm() int32 {
func KeyClicked(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -166,8 +191,8 @@ func KeyClicked(kc sdl.Keycode) bool {
func KeyReleased(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -176,8 +201,8 @@ func KeyReleased(kc sdl.Keycode) bool {
func KeyDown(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -186,8 +211,8 @@ func KeyDown(kc sdl.Keycode) bool {
func KeyUp(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
ks, ok := keyMap[kc]
if !ok {
return true
}
@ -196,8 +221,8 @@ func KeyUp(kc sdl.Keycode) bool {
func MouseClicked(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -206,8 +231,8 @@ func MouseClicked(mb int) bool {
func MouseDoubleClicked(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -215,8 +240,8 @@ func MouseDoubleClicked(mb int) bool {
}
func MouseReleased(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -225,8 +250,8 @@ func MouseReleased(mb int) bool {
func MouseDown(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -235,8 +260,8 @@ func MouseDown(mb int) bool {
func MouseUp(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
btn, ok := mouseBtnMap[mb]
if !ok {
return true
}

17
level/level.go Executable file
View File

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

1466
main.go

File diff suppressed because it is too large Load Diff

180
materials/material.go Executable file
View File

@ -0,0 +1,180 @@
package materials
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/logging"
"github.com/bloeys/nmage/shaders"
"github.com/go-gl/gl/v4.1-core/gl"
)
type TextureSlot uint32
const (
TextureSlot_Diffuse TextureSlot = 0
TextureSlot_Specular TextureSlot = 1
TextureSlot_Normal TextureSlot = 2
TextureSlot_Emission TextureSlot = 3
TextureSlot_Cubemap TextureSlot = 10
TextureSlot_ShadowMap TextureSlot = 11
TextureSlot_Cubemap_Array TextureSlot = 12
)
type Material struct {
Name string
ShaderProg shaders.ShaderProgram
UnifLocs map[string]int32
AttribLocs map[string]int32
// @TODO do this in a better way. Perhaps something like how we do fbo attachments
// Phong shading
DiffuseTex uint32
SpecularTex uint32
NormalTex uint32
EmissionTex uint32
// Shininess of specular highlights
Shininess float32
// Cubemaps
CubemapTex uint32
CubemapArrayTex uint32
// Shadowmaps
ShadowMapTex1 uint32
}
func (m *Material) Bind() {
m.ShaderProg.Bind()
if m.DiffuseTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Diffuse))
gl.BindTexture(gl.TEXTURE_2D, m.DiffuseTex)
}
if m.SpecularTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Specular))
gl.BindTexture(gl.TEXTURE_2D, m.SpecularTex)
}
if m.NormalTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Normal))
gl.BindTexture(gl.TEXTURE_2D, m.NormalTex)
}
if m.EmissionTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Emission))
gl.BindTexture(gl.TEXTURE_2D, m.EmissionTex)
}
if m.CubemapTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Cubemap))
gl.BindTexture(gl.TEXTURE_CUBE_MAP, m.CubemapTex)
}
if m.CubemapArrayTex != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Cubemap_Array))
gl.BindTexture(gl.TEXTURE_CUBE_MAP_ARRAY, m.CubemapArrayTex)
}
if m.ShadowMapTex1 != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_ShadowMap))
gl.BindTexture(gl.TEXTURE_2D, m.ShadowMapTex1)
}
}
func (m *Material) UnBind() {
gl.UseProgram(0)
}
func (m *Material) GetAttribLoc(attribName string) int32 {
loc, ok := m.AttribLocs[attribName]
if ok {
return loc
}
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) 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) {
gl.EnableVertexAttribArray(uint32(m.GetAttribLoc(attribName)))
}
func (m *Material) DisableAttribute(attribName string) {
gl.DisableVertexAttribArray(uint32(m.GetAttribLoc(attribName)))
}
func (m *Material) SetUnifInt32(uniformName string, val int32) {
gl.ProgramUniform1i(m.ShaderProg.Id, m.GetUnifLoc(uniformName), val)
}
func (m *Material) SetUnifFloat32(uniformName string, val float32) {
gl.ProgramUniform1f(m.ShaderProg.Id, m.GetUnifLoc(uniformName), val)
}
func (m *Material) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
gl.ProgramUniform2fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec2.Data[0])
}
func (m *Material) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
gl.ProgramUniform3fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec3.Data[0])
}
func (m *Material) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
gl.ProgramUniform4fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec4.Data[0])
}
func (m *Material) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
gl.ProgramUniformMatrix2fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat2.Data[0][0])
}
func (m *Material) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
gl.ProgramUniformMatrix3fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat3.Data[0][0])
}
func (m *Material) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
gl.ProgramUniformMatrix4fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat4.Data[0][0])
}
func (m *Material) Delete() {
gl.DeleteProgram(m.ShaderProg.Id)
}
func NewMaterial(matName, shaderPath string) *Material {
shdrProg, err := shaders.LoadAndCompileCombinedShader(shaderPath)
if err != nil {
logging.ErrLog.Fatalf("Failed to create new material '%s'. Err: %s\n", matName, err.Error())
}
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.Fatalf("Failed to create new material '%s'. Err: %s\n", matName, err.Error())
}
return &Material{Name: matName, ShaderProg: shdrProg, UnifLocs: make(map[string]int32), AttribLocs: make(map[string]int32)}
}

199
meshes/mesh.go Executable file
View File

@ -0,0 +1,199 @@
package meshes
import (
"errors"
"github.com/bloeys/assimp-go/asig"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/buffers"
)
type SubMesh struct {
BaseVertex int32
BaseIndex uint32
IndexCount int32
}
type Mesh struct {
Name string
Vao buffers.VertexArray
SubMeshes []SubMesh
}
func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh, error) {
scene, release, err := asig.ImportFile(modelPath, asig.PostProcessTriangulate|postProcessFlags)
if err != nil {
return nil, errors.New("Failed to load model. Err: " + err.Error())
}
defer release()
if len(scene.Meshes) == 0 {
return nil, errors.New("No meshes found in file: " + modelPath)
}
mesh := &Mesh{
Name: name,
Vao: buffers.NewVertexArray(),
SubMeshes: make([]SubMesh, 0, 1),
}
vbo := buffers.NewVertexBuffer()
ibo := buffers.NewIndexBuffer()
// 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)
// fmt.Printf("\nMesh %s has %d meshe(s) with first mesh having %d vertices\n", name, len(scene.Meshes), len(scene.Meshes[0].Vertices))
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))
}
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})
}
if i == 0 {
vbo.SetLayout(layoutToUse...)
} else {
// @TODO @NOTE: This requirement is because we are using one VAO+VBO for all
// the meshes and so the buffer must have one format.
//
// If we want to allow different layouts then we can simply create one vbo per layout and put
// meshes of the same layout in the same vbo, and we store the index of the vbo the mesh
// uses in the submesh struct.
firstSubmeshLayout := vbo.GetLayout()
assert.T(len(firstSubmeshLayout) == len(layoutToUse), "Vertex layout of submesh '%d' of mesh '%s' at path '%s' does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, name, modelPath, firstSubmeshLayout, layoutToUse)
for i := 0; i < len(firstSubmeshLayout); i++ {
assert.T(firstSubmeshLayout[i].ElementType == layoutToUse[i].ElementType, "Vertex layout of submesh '%d' of mesh '%s' at path '%s' does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, name, modelPath, 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) / vbo.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...)
}
vbo.SetData(vertexBufData, buffers.BufUsage_Static)
ibo.SetData(indexBufData)
mesh.Vao.AddVertexBuffer(vbo)
mesh.Vao.SetIndexBuffer(ibo)
// This is needed so that if you load meshes one after the other the
// following mesh doesn't attach its vbo/ibo to this vao
mesh.Vao.UnBind()
return mesh, nil
}
func v3sToV2s(v3s []gglm.Vec3) []gglm.Vec2 {
v2s := make([]gglm.Vec2, len(v3s))
for i := 0; i < len(v3s); i++ {
v2s[i] = gglm.Vec2{
Data: [2]float32{v3s[i].X(), v3s[i].Y()},
}
}
return v2s
}
type arrToInterleave struct {
V2s []gglm.Vec2
V3s []gglm.Vec3
V4s []gglm.Vec4
}
func (a *arrToInterleave) get(i int) []float32 {
assert.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
if len(a.V2s) > 0 {
return a.V2s[i].Data[:]
} else if len(a.V3s) > 0 {
return a.V3s[i].Data[:]
} else {
return a.V4s[i].Data[:]
}
}
func interleave(arrs ...arrToInterleave) []float32 {
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 {
elementCount = len(arrs[0].V2s)
} else if len(arrs[0].V3s) > 0 {
elementCount = len(arrs[0].V3s)
} else {
elementCount = len(arrs[0].V4s)
}
//Calculate final size of the float buffer
totalSize := 0
for i := 0; i < len(arrs); i++ {
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
} else if len(arrs[i].V3s) > 0 {
totalSize += len(arrs[i].V3s) * 3
} else {
totalSize += len(arrs[i].V4s) * 4
}
}
out := make([]float32, 0, totalSize)
for i := 0; i < elementCount; i++ {
for arrToUse := 0; arrToUse < len(arrs); arrToUse++ {
out = append(out, arrs[arrToUse].get(i)...)
}
}
return out
}
func flattenFaces(faces []asig.Face) []uint32 {
assert.T(len(faces[0].Indices) == 3, "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++ {
uints[i*3+0] = uint32(faces[i].Indices[0])
uints[i*3+1] = uint32(faces[i].Indices[1])
uints[i*3+2] = uint32(faces[i].Indices[2])
}
return uints
}

73
registry/iterator.go Executable file
View File

@ -0,0 +1,73 @@
package registry
// Iterator goes through the entire registry it was created from and
// returns all alive items, and nil after its done.
//
// The iterator will still work if items are added/removed to the registry
// after it was created, but the following conditions apply:
// - If items are removed, iterator will not show the removed items (assuming it didn't return them before their removal)
// - If items are added, the iterator will either only return older items (i.e. is not affected), or only return newer items (i.e. items that were going to be returned before will now not get returned in favor of newly inserted items), or a mix of old and new items.
// However, in all cases the iterator will *never* returns more items than were alive at the time of the iterator's creation.
// - If items were both added and removed, the iterator might follow either of the previous 2 cases or a combination of them
//
// To summarize: The iterator will *never* return more items than were alive at the time of its creation, and will *never* return freed items
//
// Example usage:
//
// for item, handle := it.Next(); !it.IsDone(); item, handle = it.Next() {
// // Do stuff
// }
type Iterator[T any] struct {
registry *Registry[T]
remainingItems uint
currIndex int
}
func (it *Iterator[T]) Next() (*T, Handle) {
if it.IsDone() {
return nil, 0
}
// If IsDone() only checked 'remainingItems', then when Next() returns the last item IsDone() will immediately be true which will cause loops to exit before processing the last item!
// However, with this check IsDone will remain false until Next() is called at least one more time after returning the last item which ensures the last item is processed in the loop.
//
// In cases where iterator is created on an empty registry, IsDone() will report true and the above check will return early
if it.remainingItems == 0 {
it.currIndex = -1
return nil, 0
}
for ; it.currIndex < len(it.registry.Handles); it.currIndex++ {
handle := it.registry.Handles[it.currIndex]
if !handle.HasFlag(HandleFlag_Alive) {
continue
}
item := &it.registry.Items[it.currIndex]
it.currIndex++
it.remainingItems--
return item, handle
}
// If we reached here means we iterated to the end and didn't find anything, which probably
// means that the registry changed since we were created, and that remainingItems is not accurate.
//
// As such, we zero remaining items so that this iterator is considered done
it.currIndex = -1
it.remainingItems = 0
return nil, 0
}
func (it *Iterator[T]) IsDone() bool {
if it.remainingItems != 0 {
return false
}
// We have two cases here:
// 1. Index of zero means Next() never returned an item. Remaining items of zero without returning anything means we have an empty registry and so its safe to report done
// 2. Negative index means Next() has detected we reached the end and that its safe to report being done
return it.currIndex <= 0
}

135
registry/registry.go Executable file
View File

@ -0,0 +1,135 @@
package registry
import (
"math"
"github.com/bloeys/nmage/assert"
)
type freeListitem struct {
ItemIndex uint64
nextFree *freeListitem
}
// Registry is a storage data structure that can efficiently create/get/free items using generational indices.
// Each item stored in the registry is associated with a 'handle' object that is used to get and free objects
//
// The registry 'owns' all items it stores and returns pointers to items in its array. All items are allocated upfront.
//
// It is NOT safe to concurrently create or free items. However, it is SAFE to concurrently get items
type Registry[T any] struct {
ItemCount uint
Handles []Handle
Items []T
FreeList *freeListitem
FreeListSize uint32
// The number of slots required to be in the free list before the free list
// is used for creating new entries
FreeListUsageThreshold uint32
}
func (r *Registry[T]) New() (*T, Handle) {
assert.T(r.ItemCount < uint(len(r.Handles)), "Can not add more entities to registry because it is full")
var index uint64 = math.MaxUint64
// Find index to use for the new item
if r.FreeList != nil && r.FreeListSize > r.FreeListUsageThreshold {
index = r.FreeList.ItemIndex
r.FreeList = r.FreeList.nextFree
r.FreeListSize--
} else {
for i := 0; i < len(r.Handles); i++ {
handle := r.Handles[i]
if handle.HasFlag(HandleFlag_Alive) {
continue
}
index = uint64(i)
break
}
}
if index == math.MaxUint64 {
panic("failed to create new entity because we did not find a free spot in the registry. Why did the item count assert not go off?")
}
var newItem T
newHandle := NewHandle(r.Handles[index].Generation()+1, HandleFlag_Alive, index)
assert.T(newHandle != 0, "Entity handle must not be zero")
r.ItemCount++
r.Handles[index] = newHandle
r.Items[index] = newItem
// It is very important we return directly from the items array, because if we return
// a pointer to newItem, and T is a value not a pointer, then newItem and what's stored in items will be different
return &r.Items[index], newHandle
}
func (r *Registry[T]) Get(id Handle) *T {
if id.IsZero() {
return nil
}
index := id.Index()
assert.T(index < uint64(len(r.Handles)), "Failed to get entity because of invalid entity handle. Handle index is %d while registry only has %d slots. Handle: %+v", index, r.ItemCount, id)
handle := r.Handles[index]
if handle.Generation() != id.Generation() || !handle.HasFlag(HandleFlag_Alive) {
return nil
}
item := &r.Items[index]
return item
}
// Free resets the entity flags then adds this entity to the free list
func (r *Registry[T]) Free(id Handle) {
index := id.Index()
assert.T(index < uint64(len(r.Handles)), "Failed to free entity because of invalid entity handle. Handle index is %d while registry only has %d slots. Handle: %+v", index, r.ItemCount, id)
// Nothing to do if already free
handle := r.Handles[index]
if handle.Generation() != id.Generation() || !handle.HasFlag(HandleFlag_Alive) {
return
}
// Generation is incremented on aquire, so here we just reset flags
r.ItemCount--
r.Handles[index] = NewHandle(id.Generation(), HandleFlag_None, index)
// Add to free list
r.FreeList = &freeListitem{
ItemIndex: index,
nextFree: r.FreeList,
}
r.FreeListSize++
}
func (r *Registry[T]) NewIterator() Iterator[T] {
return Iterator[T]{
registry: r,
remainingItems: r.ItemCount,
currIndex: 0,
}
}
func NewRegistry[T any](size uint32) *Registry[T] {
assert.T(size > 0, "Registry size must be more than zero")
return &Registry[T]{
Handles: make([]Handle, size),
Items: make([]T, size),
FreeListUsageThreshold: 30,
}
}

43
registry/registry_handle.go Executable file
View File

@ -0,0 +1,43 @@
package registry
type HandleFlag byte
const (
HandleFlag_None HandleFlag = 0
HandleFlag_Alive HandleFlag = 1 << (iota - 1)
)
const (
GenerationShiftBits = 64 - 8
FlagsShiftBits = 64 - 16
IndexBitMask = 0x00_00_FFFF_FFFF_FFFF
)
// Byte 1: Generation; Byte 2: Flags; Bytes 3-8: Index
type Handle uint64
// IsZero reports whether the handle is in its default 'zero' state.
// A zero handle is an invalid handle that does NOT point to any entity
func (h Handle) IsZero() bool {
return h == 0
}
func (h Handle) HasFlag(ef HandleFlag) bool {
return h.Flags()&ef > 0
}
func (h Handle) Generation() byte {
return byte(h >> GenerationShiftBits)
}
func (h Handle) Flags() HandleFlag {
return HandleFlag(h >> FlagsShiftBits)
}
func (h Handle) Index() uint64 {
return uint64(h & IndexBitMask)
}
func NewHandle(generation byte, flags HandleFlag, index uint64) Handle {
return Handle(index | (uint64(generation) << GenerationShiftBits) | (uint64(flags) << FlagsShiftBits))
}

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

@ -0,0 +1,78 @@
package rend3dgl
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/buffers"
"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 {
BoundVao *buffers.VertexArray
BoundMesh *meshes.Mesh
BoundMat *materials.Material
}
func (r *Rend3DGL) DrawMesh(mesh *meshes.Mesh, modelMat *gglm.TrMat, mat *materials.Material) {
if mesh != r.BoundMesh {
mesh.Vao.Bind()
r.BoundMesh = mesh
}
if mat != r.BoundMat {
mat.Bind()
r.BoundMat = mat
}
mat.SetUnifMat4("modelMat", &modelMat.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 (r *Rend3DGL) DrawVertexArray(mat *materials.Material, vao *buffers.VertexArray, firstElement int32, elementCount int32) {
if vao != r.BoundVao {
vao.Bind()
r.BoundVao = vao
}
if mat != r.BoundMat {
mat.Bind()
r.BoundMat = mat
}
gl.DrawArrays(gl.TRIANGLES, firstElement, elementCount)
}
func (r *Rend3DGL) DrawCubemap(mesh *meshes.Mesh, mat *materials.Material) {
if mesh != r.BoundMesh {
mesh.Vao.Bind()
r.BoundMesh = mesh
}
if mat != r.BoundMat {
mat.Bind()
r.BoundMat = mat
}
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{}
}

15
renderer/renderer.go Executable file
View File

@ -0,0 +1,15 @@
package renderer
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/meshes"
)
type Render interface {
DrawMesh(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material)
DrawVertexArray(mat *materials.Material, vao *buffers.VertexArray, firstElement int32, count int32)
DrawCubemap(mesh *meshes.Mesh, mat *materials.Material)
FrameEnd()
}

BIN
res/models/chair.fbx Executable file

Binary file not shown.

Binary file not shown.

BIN
res/models/cube.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

BIN
res/models/plane.fbx Executable file

Binary file not shown.

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

BIN
res/models/sphere.fbx Executable file

Binary file not shown.

Binary file not shown.

53
res/shaders/debug-depth.glsl Executable file
View File

@ -0,0 +1,53 @@
//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 projViewMat;
void main()
{
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
vertUV0 = vertUV0In;
vertColor = vertColorIn;
vec4 modelVert = modelMat * vec4(vertPosIn, 1);
fragPos = modelVert.xyz;
gl_Position = projViewMat * modelVert;
}
//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

@ -0,0 +1,21 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
uniform mat4 modelMat;
uniform mat4 projViewMat;
void main()
{
gl_Position = projViewMat * modelMat * vec4(vertPosIn, 1);
}
//shader:fragment
#version 410
void main()
{
// This implicitly writes to the depth buffer with no color operations
// Equivalent: gl_FragDepth = gl_FragCoord.z;
}

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);
}

47
res/shaders/imgui.glsl Executable file
View File

@ -0,0 +1,47 @@
//shader:vertex
#version 410
uniform mat4 ProjMtx;
in vec2 Position;
in vec2 UV;
in vec4 Color;
out vec2 Frag_UV;
out vec4 Frag_Color;
// Imgui doesn't handle srgb correctly, and looks too bright and wrong in srgb buffers (see: https://github.com/ocornut/imgui/issues/578).
// While not a complete fix (that would require changes in imgui itself), moving incoming srgba colors to linear in the vertex shader helps make things look better.
vec4 srgba_to_linear(vec4 srgbaColor){
#define gamma_correction 2.2
return vec4(
pow(srgbaColor.r, gamma_correction),
pow(srgbaColor.g, gamma_correction),
pow(srgbaColor.b, gamma_correction),
srgbaColor.a
);
}
void main()
{
Frag_UV = UV;
Frag_Color = srgba_to_linear(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);
}

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

@ -0,0 +1,65 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
uniform mat4 modelMat;
void main()
{
gl_Position = modelMat * vec4(vertPosIn, 1);
}
//shader:geometry
#version 410
layout (triangles) in;
// Cubemap means 6 faces, and the
// input 3 triangle vertices are drawn once per face, so 6*3=18
layout (triangle_strip, max_vertices=18) out;
uniform int cubemapIndex;
uniform mat4 cubemapProjViewMats[6];
out vec4 FragPos;
void main()
{
for(int face = 0; face < 6; ++face)
{
// Built in variable that specifies which cubemap face we are rendering to
// and only works when a cubemap is attached to the active fbo.
//
// We use an additional index here because our fbo has a cubemap array
gl_Layer = (cubemapIndex * 6) + face;
// Transform each triangle vertex
for(int i = 0; i < 3; ++i)
{
FragPos = gl_in[i].gl_Position;
gl_Position = cubemapProjViewMats[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
//shader:fragment
#version 410
in vec4 FragPos;
uniform vec3 lightPos;
uniform float farPlane;
void main()
{
// Get distance between fragment and light source
float lightDistance = length(FragPos.xyz - lightPos);
// Map to [0, 1] by dividing by far plane and use it as our depth
lightDistance = lightDistance / farPlane;
gl_FragDepth = lightDistance;
}

45
res/shaders/screen-quad.glsl Executable file
View File

@ -0,0 +1,45 @@
//shader:vertex
#version 410
out vec2 vertUV0;
// Hardcoded vertex positions for a fullscreen quad.
// Format: vec4(pos.x, pos.y, uv0.x, uv0.y)
vec4 quadData[6] = vec4[](
vec4(-1.0, 1.0, 0.0, 1.0),
vec4(-1.0, -1.0, 0.0, 0.0),
vec4(1.0, -1.0, 1.0, 0.0),
vec4(-1.0, 1.0, 0.0, 1.0),
vec4(1.0, -1.0, 1.0, 0.0),
vec4(1.0, 1.0, 1.0, 1.0)
);
uniform vec2 scale = vec2(1, 1);
uniform vec2 offset = vec2(0, 0);
void main()
{
vec4 vertData = quadData[gl_VertexID];
vertUV0 = vertData.zw;
gl_Position = vec4((vertData.xy * scale) + offset, 0.0, 1.0);
}
//shader:fragment
#version 410
struct Material {
sampler2D diffuse;
};
uniform Material material;
in vec2 vertUV0;
out vec4 fragColor;
void main()
{
vec4 diffuseTexColor = texture(material.diffuse, vertUV0);
fragColor = vec4(diffuseTexColor.rgb, 1);
}

54
res/shaders/simple-unlit.glsl Executable file
View File

@ -0,0 +1,54 @@
//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 projViewMat;
void main()
{
// @TODO: Calculate this on the CPU and send it as a uniform
//
// This produces the normal matrix that multiplies with the model normal to produce the
// world space normal. Based on 'One last thing' section from: https://learnopengl.com/Lighting/Basic-Lighting
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
vertUV0 = vertUV0In;
vertColor = vertColorIn;
vec4 modelVert = modelMat * vec4(vertPosIn, 1);
fragPos = modelVert.xyz;
gl_Position = projViewMat * modelVert;
}
//shader:fragment
#version 410
struct Material {
sampler2D diffuse;
};
uniform Material material;
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
in vec3 fragPos;
out vec4 fragColor;
void main()
{
vec4 diffuseTexColor = texture(material.diffuse, vertUV0);
fragColor = vec4(diffuseTexColor.rgb, 1);
}

View File

@ -1,23 +0,0 @@
#version 410
in vec3 vertColor;
in vec3 vertNormal;
in vec3 fragPos;
out vec4 fragColor;
uniform float ambientStrength = 0.1;
uniform vec3 ambientLightColor = vec3(1, 1, 1);
uniform vec3 lightPos1;
uniform vec3 lightColor1;
void main()
{
// vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos1 - fragPos);
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
fragColor = vec4(vertColor * (finalAmbientColor + diffStrength*lightColor1), 1.0);
}

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

@ -0,0 +1,264 @@
//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;
out vec4 fragPosDirLight;
uniform mat4 modelMat;
uniform mat4 projViewMat;
uniform mat4 dirLightProjViewMat;
void main()
{
// @TODO: Calculate this on the CPU and send it as a uniform
//
// This produces the normal matrix that multiplies with the model normal to produce the
// world space normal. Based on 'One last thing' section from: https://learnopengl.com/Lighting/Basic-Lighting
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
vertUV0 = vertUV0In;
vertColor = vertColorIn;
vec4 modelVert = modelMat * vec4(vertPosIn, 1);
fragPos = modelVert.xyz;
fragPosDirLight = dirLightProjViewMat * vec4(fragPos, 1);
gl_Position = projViewMat * modelVert;
}
//shader:fragment
#version 410
struct Material {
sampler2D diffuse;
sampler2D specular;
// sampler2D normal;
sampler2D emission;
float shininess;
};
uniform Material material;
struct DirLight {
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
sampler2D shadowMap;
};
uniform DirLight dirLight;
struct PointLight {
vec3 pos;
vec3 diffuseColor;
vec3 specularColor;
float constant;
float linear;
float quadratic;
float farPlane;
};
#define NUM_POINT_LIGHTS 8
uniform PointLight pointLights[NUM_POINT_LIGHTS];
uniform samplerCubeArray pointLightCubeShadowMaps;
struct SpotLight {
vec3 pos;
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
float innerCutoff;
float outerCutoff;
};
#define NUM_SPOT_LIGHTS 4
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
uniform vec3 camPos;
uniform vec3 ambientColor = vec3(0.2, 0.2, 0.2);
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
in vec3 fragPos;
in vec4 fragPosDirLight;
out vec4 fragColor;
// Global variables used as cache for lighting calculations
vec4 diffuseTexColor;
vec4 specularTexColor;
vec4 emissionTexColor;
vec3 normalizedVertNorm;
vec3 viewDir;
float CalcDirShadow(sampler2D shadowMap, vec3 lightDir)
{
// Move from clip space to NDC
vec3 projCoords = fragPosDirLight.xyz / fragPosDirLight.w;
// Move from [-1,1] to [0, 1]
projCoords = projCoords * 0.5 + 0.5;
// If sampling outside the depth texture then force 'no shadow'
if(projCoords.z > 1)
return 0;
// currentDepth is the fragment depth from the light's perspective
float currentDepth = projCoords.z;
// Bias in the range [0.005, 0.05] depending on the angle, where a higher
// angle gives a higher bias, as shadow acne gets worse with angle
float bias = max(0.05 * (1 - dot(normalizedVertNorm, lightDir)), 0.005);
// 'Percentage Close Filtering'.
// Basically get soft shadows by averaging this texel and surrounding ones
float shadow = 0;
vec2 texelSize = 1 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
// If our depth is larger than the lights closest depth at the texel we checked (projCoords),
// then there is something closer to the light than us, and so we are in shadow
shadow += currentDepth - bias > pcfDepth ? 1 : 0;
}
}
shadow /= 9;
return shadow;
}
vec3 CalcDirLight()
{
vec3 lightDir = normalize(-dirLight.dir);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, lightDir));
vec3 finalDiffuse = diffuseAmount * dirLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(lightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * dirLight.specularColor * specularTexColor.rgb;
// Shadow
float shadow = CalcDirShadow(dirLight.shadowMap, lightDir);
return (finalDiffuse + finalSpecular) * (1 - shadow);
}
float CalcPointShadow(int lightIndex, vec3 lightPos, vec3 lightDir, float farPlane) {
vec3 lightToFrag = fragPos - lightPos;
float closestDepth = texture(pointLightCubeShadowMaps, vec4(lightToFrag, lightIndex)).r;
// We stored depth in the cubemap in the range [0, 1], so now we move back to [0, farPlane]
closestDepth *= farPlane;
// Get depth of current fragment
float currentDepth = length(lightToFrag);
float bias = max(0.05 * (1 - dot(normalizedVertNorm, lightDir)), 0.005);
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
return shadow;
}
vec3 CalcPointLight(PointLight pointLight, int lightIndex)
{
// Ignore unset lights
if (pointLight.constant == 0){
return vec3(0);
}
vec3 lightDir = normalize(pointLight.pos - fragPos);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, lightDir));
vec3 finalDiffuse = diffuseAmount * pointLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(lightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * pointLight.specularColor * specularTexColor.rgb;
// Attenuation
float distToLight = length(pointLight.pos - fragPos);
float attenuation = 1 / (pointLight.constant + pointLight.linear * distToLight + pointLight.quadratic * (distToLight * distToLight));
// Shadow
float shadow = CalcPointShadow(lightIndex, pointLight.pos, lightDir, pointLight.farPlane);
return (finalDiffuse + finalSpecular) * attenuation * (1 - shadow);
}
vec3 CalcSpotLight(SpotLight light)
{
if (light.innerCutoff == 0)
return vec3(0);
vec3 fragToLightDir = normalize(light.pos - fragPos);
// Spot light cone with full intensity within inner cutoff,
// and falloff between inner-outer cutoffs, and zero
// light after outer cutoff
float theta = dot(fragToLightDir, normalize(-light.dir));
float epsilon = (light.innerCutoff - light.outerCutoff);
float intensity = clamp((theta - light.outerCutoff) / epsilon, float(0), float(1));
if (intensity == 0)
return vec3(0);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, fragToLightDir));
vec3 finalDiffuse = diffuseAmount * light.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(fragToLightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * light.specularColor * specularTexColor.rgb;
return (finalDiffuse + finalSpecular) * intensity;
}
void main()
{
// Shared values
diffuseTexColor = texture(material.diffuse, vertUV0);
specularTexColor = texture(material.specular, vertUV0);
emissionTexColor = texture(material.emission, vertUV0);
normalizedVertNorm = normalize(vertNormal);
viewDir = normalize(camPos - fragPos);
// Light contributions
vec3 finalColor = CalcDirLight();
for (int i = 0; i < NUM_POINT_LIGHTS; i++)
{
finalColor += CalcPointLight(pointLights[i], i);
}
for (int i = 0; i < NUM_SPOT_LIGHTS; i++)
{
finalColor += CalcSpotLight(spotLights[i]);
}
vec3 finalEmission = emissionTexColor.rgb;
vec3 finalAmbient = ambientColor * diffuseTexColor.rgb;
fragColor = vec4(finalColor + finalAmbient + finalEmission, 1);
}

View File

@ -1,23 +0,0 @@
#version 410
in vec3 vertPosIn;
in vec3 vertColorIn;
in vec3 vertNormalIn;
out vec3 vertColor;
out vec3 vertNormal;
out vec3 fragPos;
//MVP = Model View Projection
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
void main()
{
vertColor = vertColorIn;
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
}

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

@ -0,0 +1,32 @@
//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 projViewMat;
void main()
{
vertUV0 = vec3(vertPosIn.x, vertPosIn.y, -vertPosIn.z);
vec4 pos = projViewMat * 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

BIN
res/textures/black.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 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: 1.0 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 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: 770 KiB

BIN
res/textures/white.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
rsrc_windows_386.syso Executable file

Binary file not shown.

BIN
rsrc_windows_amd64.syso Executable file

Binary file not shown.

View File

@ -1,110 +1,53 @@
package shaders
import (
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/buffers"
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type ShaderProgram struct {
ID uint32
VertShaderID uint32
FragShaderID uint32
Id uint32
VertShaderId uint32
FragShaderId uint32
GeomShaderId uint32
}
func (sp *ShaderProgram) AttachShader(shader Shader) {
gl.AttachShader(sp.ID, shader.ID)
switch shader.ShaderType {
case VertexShaderType:
sp.VertShaderID = shader.ID
case FragmentShaderType:
sp.FragShaderID = shader.ID
gl.AttachShader(sp.Id, shader.Id)
switch shader.Type {
case ShaderType_Vertex:
sp.VertShaderId = shader.Id
case ShaderType_Fragment:
sp.FragShaderId = shader.Id
case ShaderType_Geometry:
sp.GeomShaderId = shader.Id
default:
logging.ErrLog.Println("Unknown shader type ", shader.ShaderType, " for ID ", shader.ID)
logging.ErrLog.Fatalf("Unknown shader type '%d' for shader id '%d'\n", shader.Type, shader.Id)
}
}
func (sp *ShaderProgram) Link() {
gl.LinkProgram(sp.ID)
gl.LinkProgram(sp.Id)
if sp.VertShaderID != 0 {
gl.DeleteShader(sp.VertShaderID)
if sp.VertShaderId != 0 {
gl.DeleteShader(sp.VertShaderId)
}
if sp.FragShaderID != 0 {
gl.DeleteShader(sp.FragShaderID)
if sp.FragShaderId != 0 {
gl.DeleteShader(sp.FragShaderId)
}
if sp.GeomShaderId != 0 {
gl.DeleteShader(sp.GeomShaderId)
}
}
func (sp *ShaderProgram) Activate() {
gl.UseProgram(sp.ID)
func (s *ShaderProgram) Bind() {
gl.UseProgram(s.Id)
}
func (sp *ShaderProgram) Deactivate() {
func (s *ShaderProgram) UnBind() {
gl.UseProgram(0)
}
func (sp *ShaderProgram) GetAttribLoc(attribName string) int32 {
return gl.GetAttribLocation(sp.ID, gl.Str(attribName+"\x00"))
}
func (sp *ShaderProgram) SetAttribute(attribName string, bufObj *buffers.BufferObject, buf *buffers.Buffer) {
bufObj.Activate()
buf.Activate()
attribLoc := sp.GetAttribLoc(attribName)
gl.VertexAttribPointer(uint32(attribLoc), buf.ElementCount, buf.ElementType, false, buf.GetSize(), gl.PtrOffset(0))
bufObj.Activate()
buf.Deactivate()
}
func (sp *ShaderProgram) EnableAttribute(attribName string) {
gl.EnableVertexAttribArray(uint32(sp.GetAttribLoc(attribName)))
}
func (sp *ShaderProgram) DisableAttribute(attribName string) {
gl.DisableVertexAttribArray(uint32(sp.GetAttribLoc(attribName)))
}
func (sp *ShaderProgram) SetUnifFloat32(uniformName string, val float32) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform1f(sp.ID, loc, val)
}
func (sp *ShaderProgram) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform2fv(sp.ID, loc, 1, &vec2.Data[0])
}
func (sp *ShaderProgram) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform3fv(sp.ID, loc, 1, &vec3.Data[0])
}
func (sp *ShaderProgram) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniform4fv(sp.ID, loc, 1, &vec4.Data[0])
}
func (sp *ShaderProgram) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix2fv(sp.ID, loc, 1, false, &mat2.Data[0][0])
}
func (sp *ShaderProgram) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix3fv(sp.ID, loc, 1, false, &mat3.Data[0][0])
}
func (sp *ShaderProgram) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
gl.ProgramUniformMatrix4fv(sp.ID, loc, 1, false, &mat4.Data[0][0])
}
func (sp *ShaderProgram) Delete() {
gl.DeleteProgram(sp.ID)
}

View File

@ -1,10 +1,31 @@
package shaders
import "github.com/go-gl/gl/v4.1-core/gl"
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type ShaderType int
type ShaderType int32
func (s ShaderType) ToGl() uint32 {
switch s {
case ShaderType_Vertex:
return gl.VERTEX_SHADER
case ShaderType_Fragment:
return gl.FRAGMENT_SHADER
case ShaderType_Geometry:
return gl.GEOMETRY_SHADER
default:
logging.ErrLog.Fatalf("Unknown shader type '%d'\n", s)
return 0
}
}
const (
VertexShaderType ShaderType = gl.VERTEX_SHADER
FragmentShaderType ShaderType = gl.FRAGMENT_SHADER
ShaderType_Unknown ShaderType = iota
ShaderType_Vertex
ShaderType_Fragment
ShaderType_Geometry
)

View File

@ -1,7 +1,9 @@
package shaders
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
@ -10,12 +12,13 @@ import (
)
type Shader struct {
ID uint32
ShaderType ShaderType
Id uint32
Type ShaderType
}
func (s Shader) Delete() {
gl.DeleteShader(s.ID)
func (s *Shader) Delete() {
gl.DeleteShader(s.Id)
s.Id = 0
}
func NewShaderProgram() (ShaderProgram, error) {
@ -25,52 +28,117 @@ func NewShaderProgram() (ShaderProgram, error) {
return ShaderProgram{}, errors.New("failed to create shader program")
}
return ShaderProgram{ID: id}, nil
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
}
shaderID := gl.CreateShader(uint32(shaderType))
if shaderID == 0 {
logging.ErrLog.Println("Failed to create shader.")
return Shader{}, errors.New("failed to create shader")
return LoadAndCompileCombinedShaderSrc(combinedSource)
}
func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
shaderSources := bytes.Split(shaderSrc, []byte("//shader:"))
if len(shaderSources) < 2 {
return ShaderProgram{}, errors.New("failed to read combined shader. The minimum shader types to have are '//shader:vertex' and '//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 = ShaderType_Vertex
} else if bytes.HasPrefix(src, []byte("fragment")) {
src = src[8:]
shdrType = ShaderType_Fragment
} else if bytes.HasPrefix(src, []byte("geometry")) {
src = src[8:]
shdrType = ShaderType_Geometry
} else {
return ShaderProgram{}, errors.New("unknown shader type. Must be '//shader:vertex' or '//shader:fragment' or '//shader:geometry'")
}
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' or '//shader:geometry' before your shaders")
}
if shdrProg.VertShaderId == 0 {
return ShaderProgram{}, errors.New("no valid vertex shader found. Please put '//shader:vertex' before your vertex shader")
}
if shdrProg.FragShaderId == 0 {
return ShaderProgram{}, errors.New("no valid fragment shader found. Please put '//shader:fragment' before your vertex shader")
}
shdrProg.Link()
return shdrProg, nil
}
func CompileShaderOfType(shaderSource []byte, shaderType ShaderType) (Shader, error) {
shaderId := gl.CreateShader(shaderType.ToGl())
if shaderId == 0 {
return Shader{}, fmt.Errorf("failed to create OpenGl shader. OpenGl Error=%d", gl.GetError())
}
//Load shader source and compile
shaderCStr, shaderFree := gl.Strs(string(shaderSource) + "\x00")
defer shaderFree()
gl.ShaderSource(shaderID, 1, shaderCStr, nil)
gl.ShaderSource(shaderId, 1, shaderCStr, nil)
gl.CompileShader(shaderID)
if err := getShaderCompileErrors(shaderID); err != nil {
gl.DeleteShader(shaderID)
gl.CompileShader(shaderId)
if err := getShaderCompileErrors(shaderId); err != nil {
gl.DeleteShader(shaderId)
return Shader{}, err
}
return Shader{ID: shaderID, ShaderType: shaderType}, nil
return Shader{Id: shaderId, Type: shaderType}, nil
}
func getShaderCompileErrors(shaderID uint32) error {
func getShaderCompileErrors(shaderId uint32) error {
var compiledSuccessfully int32
gl.GetShaderiv(shaderID, gl.COMPILE_STATUS, &compiledSuccessfully)
gl.GetShaderiv(shaderId, gl.COMPILE_STATUS, &compiledSuccessfully)
if compiledSuccessfully == gl.TRUE {
return nil
}
var logLength int32
gl.GetShaderiv(shaderID, gl.INFO_LOG_LENGTH, &logLength)
gl.GetShaderiv(shaderId, gl.INFO_LOG_LENGTH, &logLength)
log := gl.Str(strings.Repeat("\x00", int(logLength)))
gl.GetShaderInfoLog(shaderID, logLength, nil, log)
gl.GetShaderInfoLog(shaderId, logLength, nil, log)
errMsg := gl.GoStr(log)
logging.ErrLog.Println("Compilation of shader with id ", shaderID, " failed. Err: ", errMsg)
logging.ErrLog.Println("Compilation of shader with id ", shaderId, " failed. Err: ", errMsg)
return errors.New(errMsg)
}

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())

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

@ -0,0 +1,468 @@
package nmageimgui
import (
imgui "github.com/AllenDang/cimgui-go"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/timing"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/veandco/go-sdl2/sdl"
)
type ImguiInfo struct {
ImCtx imgui.Context
Mat *materials.Material
VaoID uint32
VboID uint32
IndexBufID uint32
// This is a pointer so we can send a stable pointer to C code
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.CurrentDrawData()
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.GetVertexBuffer()
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW)
indexBuffer, indexBufferSize := list.GetIndexBuffer()
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.ActiveTexture(gl.TEXTURE0)
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.DrawElementsBaseVertexWithOffset(gl.TRIANGLES, int32(cmd.ElemCount()), uint32(drawType), uintptr(int(cmd.IdxOffset())*indexSize), int32(cmd.VtxOffset()))
}
}
}
//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.GlyphRange) imgui.Font {
fontConfigToUse := imgui.NewFontConfig()
if fontConfig != nil {
fontConfigToUse = *fontConfig
}
glyphRangesToUse := imgui.NewGlyphRange()
if glyphRanges != nil {
glyphRangesToUse = *glyphRanges
}
imIO := imgui.CurrentIO()
a := imIO.Fonts()
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse.Data())
pixels, width, height, _ := a.GetTextureDataAsAlpha8()
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, *i.TexID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(width), int32(height), 0, gl.RED, gl.UNSIGNED_BYTE, pixels)
return f
}
const DefaultImguiShader = `
//shader:vertex
#version 410
uniform mat4 ProjMtx;
in vec2 Position;
in vec2 UV;
in vec4 Color;
out vec2 Frag_UV;
out vec4 Frag_Color;
// Imgui doesn't handle srgb correctly, and looks too bright and wrong in srgb buffers (see: https://github.com/ocornut/imgui/issues/578).
// While not a complete fix (that would require changes in imgui itself), moving incoming srgba colors to linear in the vertex shader helps make things look better.
vec4 srgba_to_linear(vec4 srgbaColor){
#define gamma_correction 2.2
return vec4(
pow(srgbaColor.r, gamma_correction),
pow(srgbaColor.g, gamma_correction),
pow(srgbaColor.b, gamma_correction),
srgbaColor.a
);
}
void main()
{
Frag_UV = UV;
Frag_Color = srgba_to_linear(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);
}
`
// NewImGui setups imgui using the passed shader.
// If the path is empty a default nMage shader is used
func NewImGui(shaderPath string) ImguiInfo {
var imguiMat *materials.Material
if shaderPath == "" {
imguiMat = materials.NewMaterialSrc("ImGUI Mat", []byte(DefaultImguiShader))
} else {
imguiMat = materials.NewMaterial("ImGUI Mat", shaderPath)
}
imguiInfo := ImguiInfo{
ImCtx: imgui.CreateContext(),
Mat: imguiMat,
TexID: new(uint32),
}
io := imgui.CurrentIO()
io.SetConfigFlags(io.ConfigFlags() | imgui.ConfigFlagsDockingEnable)
io.SetBackendFlags(io.BackendFlags() | 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.ActiveTexture(gl.TEXTURE0)
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)
pixels, width, height, _ := io.Fonts().GetTextureDataAsAlpha8()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(width), int32(height), 0, gl.RED, gl.UNSIGNED_BYTE, pixels)
// Store our identifier
io.Fonts().SetTexID(imgui.TextureID(imguiInfo.TexID))
//Shader attributes
imguiInfo.Mat.Bind()
imguiInfo.Mat.EnableAttribute("Position")
imguiInfo.Mat.EnableAttribute("UV")
imguiInfo.Mat.EnableAttribute("Color")
imguiInfo.Mat.UnBind()
return imguiInfo
}
func SdlScancodeToImGuiKey(scancode sdl.Scancode) imgui.Key {
switch scancode {
case sdl.SCANCODE_TAB:
return imgui.KeyTab
case sdl.SCANCODE_LEFT:
return imgui.KeyLeftArrow
case sdl.SCANCODE_RIGHT:
return imgui.KeyRightArrow
case sdl.SCANCODE_UP:
return imgui.KeyUpArrow
case sdl.SCANCODE_DOWN:
return imgui.KeyDownArrow
case sdl.SCANCODE_PAGEUP:
return imgui.KeyPageUp
case sdl.SCANCODE_PAGEDOWN:
return imgui.KeyPageDown
case sdl.SCANCODE_HOME:
return imgui.KeyHome
case sdl.SCANCODE_END:
return imgui.KeyEnd
case sdl.SCANCODE_INSERT:
return imgui.KeyInsert
case sdl.SCANCODE_DELETE:
return imgui.KeyDelete
case sdl.SCANCODE_BACKSPACE:
return imgui.KeyBackspace
case sdl.SCANCODE_SPACE:
return imgui.KeySpace
case sdl.SCANCODE_RETURN:
return imgui.KeyEnter
case sdl.SCANCODE_ESCAPE:
return imgui.KeyEscape
case sdl.SCANCODE_APOSTROPHE:
return imgui.KeyApostrophe
case sdl.SCANCODE_COMMA:
return imgui.KeyComma
case sdl.SCANCODE_MINUS:
return imgui.KeyMinus
case sdl.SCANCODE_PERIOD:
return imgui.KeyPeriod
case sdl.SCANCODE_SLASH:
return imgui.KeySlash
case sdl.SCANCODE_SEMICOLON:
return imgui.KeySemicolon
case sdl.SCANCODE_EQUALS:
return imgui.KeyEqual
case sdl.SCANCODE_LEFTBRACKET:
return imgui.KeyLeftBracket
case sdl.SCANCODE_BACKSLASH:
return imgui.KeyBackslash
case sdl.SCANCODE_RIGHTBRACKET:
return imgui.KeyRightBracket
case sdl.SCANCODE_GRAVE:
return imgui.KeyGraveAccent
case sdl.SCANCODE_CAPSLOCK:
return imgui.KeyCapsLock
case sdl.SCANCODE_SCROLLLOCK:
return imgui.KeyScrollLock
case sdl.SCANCODE_NUMLOCKCLEAR:
return imgui.KeyNumLock
case sdl.SCANCODE_PRINTSCREEN:
return imgui.KeyPrintScreen
case sdl.SCANCODE_PAUSE:
return imgui.KeyPause
case sdl.SCANCODE_KP_0:
return imgui.KeyKeypad0
case sdl.SCANCODE_KP_1:
return imgui.KeyKeypad1
case sdl.SCANCODE_KP_2:
return imgui.KeyKeypad2
case sdl.SCANCODE_KP_3:
return imgui.KeyKeypad3
case sdl.SCANCODE_KP_4:
return imgui.KeyKeypad4
case sdl.SCANCODE_KP_5:
return imgui.KeyKeypad5
case sdl.SCANCODE_KP_6:
return imgui.KeyKeypad6
case sdl.SCANCODE_KP_7:
return imgui.KeyKeypad7
case sdl.SCANCODE_KP_8:
return imgui.KeyKeypad8
case sdl.SCANCODE_KP_9:
return imgui.KeyKeypad9
case sdl.SCANCODE_KP_PERIOD:
return imgui.KeyKeypadDecimal
case sdl.SCANCODE_KP_DIVIDE:
return imgui.KeyKeypadDivide
case sdl.SCANCODE_KP_MULTIPLY:
return imgui.KeyKeypadMultiply
case sdl.SCANCODE_KP_MINUS:
return imgui.KeyKeypadSubtract
case sdl.SCANCODE_KP_PLUS:
return imgui.KeyKeypadAdd
case sdl.SCANCODE_KP_ENTER:
return imgui.KeyKeypadEnter
case sdl.SCANCODE_KP_EQUALS:
return imgui.KeyKeypadEqual
case sdl.SCANCODE_LSHIFT:
return imgui.KeyLeftShift
case sdl.SCANCODE_LCTRL:
return imgui.KeyLeftCtrl
case sdl.SCANCODE_LALT:
return imgui.KeyLeftAlt
case sdl.SCANCODE_LGUI:
return imgui.KeyLeftSuper
case sdl.SCANCODE_RSHIFT:
return imgui.KeyRightShift
case sdl.SCANCODE_RCTRL:
return imgui.KeyRightCtrl
case sdl.SCANCODE_RALT:
return imgui.KeyRightAlt
case sdl.SCANCODE_RGUI:
return imgui.KeyRightSuper
case sdl.SCANCODE_MENU:
return imgui.KeyMenu
case sdl.SCANCODE_0:
return imgui.Key0
case sdl.SCANCODE_1:
return imgui.Key1
case sdl.SCANCODE_2:
return imgui.Key2
case sdl.SCANCODE_3:
return imgui.Key3
case sdl.SCANCODE_4:
return imgui.Key4
case sdl.SCANCODE_5:
return imgui.Key5
case sdl.SCANCODE_6:
return imgui.Key6
case sdl.SCANCODE_7:
return imgui.Key7
case sdl.SCANCODE_8:
return imgui.Key8
case sdl.SCANCODE_9:
return imgui.Key9
case sdl.SCANCODE_A:
return imgui.KeyA
case sdl.SCANCODE_B:
return imgui.KeyB
case sdl.SCANCODE_C:
return imgui.KeyC
case sdl.SCANCODE_D:
return imgui.KeyD
case sdl.SCANCODE_E:
return imgui.KeyE
case sdl.SCANCODE_F:
return imgui.KeyF
case sdl.SCANCODE_G:
return imgui.KeyG
case sdl.SCANCODE_H:
return imgui.KeyH
case sdl.SCANCODE_I:
return imgui.KeyI
case sdl.SCANCODE_J:
return imgui.KeyJ
case sdl.SCANCODE_K:
return imgui.KeyK
case sdl.SCANCODE_L:
return imgui.KeyL
case sdl.SCANCODE_M:
return imgui.KeyM
case sdl.SCANCODE_N:
return imgui.KeyN
case sdl.SCANCODE_O:
return imgui.KeyO
case sdl.SCANCODE_P:
return imgui.KeyP
case sdl.SCANCODE_Q:
return imgui.KeyQ
case sdl.SCANCODE_R:
return imgui.KeyR
case sdl.SCANCODE_S:
return imgui.KeyS
case sdl.SCANCODE_T:
return imgui.KeyT
case sdl.SCANCODE_U:
return imgui.KeyU
case sdl.SCANCODE_V:
return imgui.KeyV
case sdl.SCANCODE_W:
return imgui.KeyW
case sdl.SCANCODE_X:
return imgui.KeyX
case sdl.SCANCODE_Y:
return imgui.KeyY
case sdl.SCANCODE_Z:
return imgui.KeyZ
case sdl.SCANCODE_F1:
return imgui.KeyF1
case sdl.SCANCODE_F2:
return imgui.KeyF2
case sdl.SCANCODE_F3:
return imgui.KeyF3
case sdl.SCANCODE_F4:
return imgui.KeyF4
case sdl.SCANCODE_F5:
return imgui.KeyF5
case sdl.SCANCODE_F6:
return imgui.KeyF6
case sdl.SCANCODE_F7:
return imgui.KeyF7
case sdl.SCANCODE_F8:
return imgui.KeyF8
case sdl.SCANCODE_F9:
return imgui.KeyF9
case sdl.SCANCODE_F10:
return imgui.KeyF10
case sdl.SCANCODE_F11:
return imgui.KeyF11
case sdl.SCANCODE_F12:
return imgui.KeyF12
default:
return imgui.KeyNone
}
}