Compare commits

...

93 Commits

Author SHA1 Message Date
581d17d1d9 Frame time graph 2024-05-13 05:18:21 +04:00
3795a7123f Ensure renderer calls aren't virtual 2024-05-13 04:57:16 +04:00
5aa0f41085 Internal material func rename 2024-05-13 04:36:53 +04:00
c782e8c312 Get rid of allocations on SetUniform calls, allowing us to pass ref again 2024-05-13 04:33:54 +04:00
f0a12879f8 Add todo 2024-05-13 03:45:16 +04:00
6ea08e9826 Get rid of more pointers to make allocs predictable 2024-05-13 03:42:52 +04:00
83c6f635e5 Show fps in debug window 2024-05-13 03:21:47 +04:00
cf6b2655e7 After all why not, why shouldn't we have HDR 2024-05-12 06:46:46 +04:00
7b1e3ea7b4 Default textures for diffuse/specular/normal/emission mat slots 2024-05-11 05:11:54 +04:00
c884d2624d Normal mapping 2024-05-07 05:23:36 +04:00
8c6b1d5821 Adjust shadow map texture sizes 2024-05-06 23:23:52 +04:00
dfd1fe9c5e Material settings+normal matrices on CPU 2024-05-06 22:55:57 +04:00
24613823a7 Fix gitignore 2024-05-06 22:18:15 +04:00
0386f441d6 Profiling 2024-05-06 22:16:20 +04:00
57ab851534 Update gglm 2024-05-05 00:34:38 +04:00
d523c0951b Get rid of unneeded pointers+update todos 2024-05-01 01:16:33 +04:00
abd7079e61 Correct modifier keys input to imgui 2024-04-24 19:56:35 +04:00
4d8ccdaf56 Captured/uncaptured mode in input package+comments 2024-04-20 11:53:06 +04:00
a131e1b52d Add todo regarding input package 2024-04-20 11:14:29 +04:00
f35c217d73 Spotlight shadows 2024-04-16 10:34:30 +04:00
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
66 changed files with 4742 additions and 894 deletions

View File

@ -1,22 +1,28 @@
name: build-nmage
on:
create:
workflow_dispatch:
jobs:
build-nmage-macos:
runs-on: macos-10.15
runs-on: macos-12
steps:
- name: Install golang 1.17
uses: actions/setup-go@v2
- name: Install golang
uses: actions/setup-go@v3
with:
go-version: '^1.17'
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 .

5
.gitignore vendored
View File

@ -15,4 +15,7 @@
vendor/
.vscode/
imgui.ini
*~
*~
# Custom
*.pprof

View File

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

View File

@ -5,8 +5,9 @@ import (
"github.com/bloeys/nmage/logging"
)
func T(check bool, msg string) {
func T(check bool, msg string, args ...any) {
if consts.Debug && !check {
logging.ErrLog.Panicln("Assert failed:", msg)
logging.ErrLog.Panicf("Assert failed: "+msg, args...)
}
}

View File

@ -1,8 +1,8 @@
package assets
var (
Textures map[uint32]Texture = make(map[uint32]Texture)
TexturePaths map[string]uint32 = make(map[string]uint32)
Textures = make(map[uint32]Texture)
TexturePaths = make(map[string]uint32)
)
func AddTextureToCache(t Texture) {

View File

@ -2,27 +2,51 @@ package assets
import (
"bytes"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"strings"
"unsafe"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/mandykoh/prism"
)
type ColorFormat int
const (
ColorFormat_RGBA8 ColorFormat = iota
ColorFormat_Unknown ColorFormat = iota
ColorFormat_RGBA8
)
var (
DefaultBlackTexId Texture
DefaultWhiteTexId Texture
DefaultDiffuseTexId Texture
DefaultSpecularTexId Texture
DefaultNormalTexId Texture
DefaultEmissionTexId Texture
)
type Texture struct {
//Path only exists for textures loaded from disk
Path string
TexID uint32
Width int32
// 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
}
@ -30,6 +54,19 @@ 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) {
@ -50,34 +87,20 @@ func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, erro
return Texture{}, err
}
img, err := png.Decode(bytes.NewReader(fileBytes))
bytesReader := bytes.NewReader(fileBytes)
img, err := png.Decode(bytesReader)
if err != nil {
return Texture{}, err
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: file,
Width: int32(img.Bounds().Dx()),
Height: int32(img.Bounds().Dy()),
Pixels: make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4),
}
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
//NOTE: We only support 8-bit channels (32-bit colors) for now
i := 0
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
for x := 0; x < img.Bounds().Dx(); x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
tex.Pixels[i] = c.R
tex.Pixels[i+1] = c.G
tex.Pixels[i+2] = c.B
tex.Pixels[i+3] = c.A
i += 4
}
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)
@ -90,7 +113,12 @@ func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, erro
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
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)
@ -100,37 +128,27 @@ func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, erro
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func LoadTextureInMemImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
func LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Width: int32(img.Bounds().Dx()),
Height: int32(img.Bounds().Dy()),
Pixels: make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4),
}
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
//NOTE: We only support 8-bit channels (32-bit colors) for now
i := 0
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
for x := 0; x < img.Bounds().Dx(); x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
tex.Pixels[i] = c.R
tex.Pixels[i+1] = c.G
tex.Pixels[i+2] = c.B
tex.Pixels[i+3] = c.A
i += 4
}
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)
@ -143,7 +161,12 @@ func LoadTextureInMemImg(img image.Image, loadOptions *TextureLoadOptions) (Text
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
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)
@ -153,5 +176,160 @@ func LoadTextureInMemImg(img image.Image, loadOptions *TextureLoadOptions) (Text
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)
}
}

View File

@ -10,8 +10,10 @@ import (
type BufUsage int
const (
BufUsage_Unknown BufUsage = iota
//Buffer is set only once and used many times
BufUsage_Static BufUsage = iota
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

View File

@ -1,131 +0,0 @@
package buffers
import (
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type Buffer struct {
VAOID uint32
//BufID is the ID of the VBO
BufID uint32
//IndexBufID is the ID of the index/element buffer
IndexBufID uint32
IndexBufCount int32
Stride int32
layout []Element
}
func (b *Buffer) Bind() {
gl.BindVertexArray(b.VAOID)
}
func (b *Buffer) UnBind() {
gl.BindVertexArray(0)
}
func (b *Buffer) SetData(values []float32) {
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static.ToGL())
} else {
gl.BufferData(gl.ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func (b *Buffer) SetDataWithUsage(values []float32, usage BufUsage) {
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ARRAY_BUFFER, 0, gl.Ptr(nil), usage.ToGL())
} else {
gl.BufferData(gl.ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), usage.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func (b *Buffer) SetIndexBufData(values []uint32) {
b.IndexBufCount = int32(len(values))
gl.BindVertexArray(b.VAOID)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, b.IndexBufID)
sizeInBytes := len(values) * 4
if sizeInBytes == 0 {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static.ToGL())
} else {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static.ToGL())
}
gl.BindVertexArray(0)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0)
}
func (b *Buffer) GetLayout() []Element {
e := make([]Element, len(b.layout))
copy(e, b.layout)
return e
}
//SetLayout updates the layout object and the corresponding vertex attributes.
//Vertex attributes are also enabled.
func (b *Buffer) SetLayout(layout ...Element) {
b.layout = layout
b.Stride = 0
for i := 0; i < len(b.layout); i++ {
b.layout[i].Offset = int(b.Stride)
b.Stride += b.layout[i].Size()
}
//Set opengl stuff
b.Bind()
//NOTE: VBOs are only bound at 'VertexAttribPointer', not BindBUffer, so we need to bind the buffer and vao here
gl.BindBuffer(gl.ARRAY_BUFFER, b.BufID)
for i := 0; i < len(layout); i++ {
gl.EnableVertexAttribArray(uint32(i))
gl.VertexAttribPointer(uint32(i), layout[i].ElementType.CompCount(), layout[i].ElementType.GLType(), false, b.Stride, gl.PtrOffset(layout[i].Offset))
}
b.UnBind()
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
}
func NewBuffer(layout ...Element) Buffer {
b := Buffer{}
gl.GenVertexArrays(1, &b.VAOID)
if b.VAOID == 0 {
logging.ErrLog.Println("Failed to create openGL vertex array object")
}
gl.GenBuffers(1, &b.BufID)
if b.BufID == 0 {
logging.ErrLog.Println("Failed to create openGL buffer")
}
gl.GenBuffers(1, &b.IndexBufID)
if b.IndexBufID == 0 {
logging.ErrLog.Println("Failed to create openGL buffer")
}
b.SetLayout(layout...)
return b
}

667
buffers/framebuffer.go Executable file
View File

@ -0,0 +1,667 @@
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_Texture_Array
FramebufferAttachmentType_Renderbuffer
FramebufferAttachmentType_Cubemap
FramebufferAttachmentType_Cubemap_Array
)
func (f FramebufferAttachmentType) IsValid() bool {
switch f {
case FramebufferAttachmentType_Texture:
fallthrough
case FramebufferAttachmentType_Texture_Array:
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_RGBAF16
FramebufferAttachmentDataFormat_SRGBA
FramebufferAttachmentDataFormat_DepthF32
FramebufferAttachmentDataFormat_Depth24Stencil8
)
func (f FramebufferAttachmentDataFormat) IsColorFormat() bool {
return f == FramebufferAttachmentDataFormat_R32Int ||
f == FramebufferAttachmentDataFormat_RGBA8 ||
f == FramebufferAttachmentDataFormat_SRGBA ||
f == FramebufferAttachmentDataFormat_RGBAF16
}
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_RGBAF16:
return gl.RGBA16F
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_RGBAF16:
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
}
}
func (f FramebufferAttachmentDataFormat) GlComponentType() uint32 {
switch f {
case FramebufferAttachmentDataFormat_R32Int:
return gl.INT
case FramebufferAttachmentDataFormat_RGBA8:
fallthrough
case FramebufferAttachmentDataFormat_SRGBA:
return gl.UNSIGNED_BYTE
case FramebufferAttachmentDataFormat_RGBAF16:
// Seems this is fine to be float instead of half float
fallthrough
case FramebufferAttachmentDataFormat_DepthF32:
return gl.FLOAT
case FramebufferAttachmentDataFormat_Depth24Stencil8:
return gl.UNSIGNED_INT_24_8
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 fbo'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 attachType == FramebufferAttachmentType_Texture_Array {
logging.ErrLog.Fatalf("failed creating color attachment because texture arrays can not be color attachments (implementation can be updated to support it or you can 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(),
attachFormat.GlComponentType(),
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")
}
if attachType == FramebufferAttachmentType_Texture_Array {
logging.ErrLog.Fatalf("failed creating texture array depth attachment because 'NewDepthTextureArrayAttachment' 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(),
attachFormat.GlComponentType(),
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(),
attachFormat.GlComponentType(),
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(),
attachFormat.GlComponentType(),
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) NewDepthTextureArrayAttachment(
attachFormat FramebufferAttachmentDataFormat,
numTextures int32,
) {
if fbo.HasDepthAttachment() {
logging.ErrLog.Fatalf("failed creating texture 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_Texture_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_2D_ARRAY, a.Id)
gl.TexImage3D(
gl.TEXTURE_2D_ARRAY,
0,
attachFormat.GlInternalFormat(),
int32(fbo.Width),
int32(fbo.Height),
numTextures,
0,
attachFormat.GlFormat(),
attachFormat.GlComponentType(),
nil,
)
gl.TexParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D_ARRAY, 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_ARRAY, gl.TEXTURE_BORDER_COLOR, &borderColor[0])
gl.TexParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER)
gl.TexParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER)
gl.BindTexture(gl.TEXTURE_2D_ARRAY, 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(),
attachFormat.GlComponentType(),
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
}

View File

@ -41,7 +41,7 @@ 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)
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
}
@ -59,9 +59,9 @@ func (c *Camera) UpdateRotation(pitch, yaw float32) {
c.Update()
}
func NewPerspective(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, fovRadians, aspectRatio float32) *Camera {
func NewPerspective(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, fovRadians, aspectRatio float32) Camera {
cam := &Camera{
cam := Camera{
Type: Type_Perspective,
Pos: *pos,
Forward: *forward,
@ -78,9 +78,9 @@ func NewPerspective(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, fovRadi
return cam
}
func NewOrthographic(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, left, right, top, bottom float32) *Camera {
func NewOrthographic(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, left, right, top, bottom float32) Camera {
cam := &Camera{
cam := Camera{
Type: Type_Orthographic,
Pos: *pos,
Forward: *forward,

View File

@ -1,32 +1,45 @@
package engine
import (
"image"
"image/color"
"runtime"
imgui "github.com/AllenDang/cimgui-go"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/assets"
"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/inkyblackness/imgui-go/v4"
"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()
imIo := imgui.CurrentIO()
imguiCaptureMouse := imIo.WantCaptureMouse()
imguiCaptureKeyboard := imIo.WantCaptureKeyboard()
input.EventLoopStart(imguiCaptureMouse, imguiCaptureKeyboard)
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
@ -41,29 +54,42 @@ func (w *Window) handleInputs() {
case *sdl.MouseWheelEvent:
input.HandleMouseWheelEvent(e)
xDelta, yDelta := input.GetMouseWheelMotion()
imIO.AddMouseWheelDelta(float32(xDelta), float32(yDelta))
imIo.AddMouseWheelDelta(float32(e.X), float32(e.Y))
case *sdl.KeyboardEvent:
input.HandleKeyboardEvent(e)
if e.Type == sdl.KEYDOWN {
imIO.KeyPress(int(e.Keysym.Scancode))
} else if e.Type == sdl.KEYUP {
imIO.KeyRelease(int(e.Keysym.Scancode))
}
// Send modifier key updates to imgui (based on the imgui SDL backend)
imIo.AddKeyEvent(imgui.ModCtrl, e.Keysym.Mod&sdl.KMOD_CTRL != 0)
imIo.AddKeyEvent(imgui.ModShift, e.Keysym.Mod&sdl.KMOD_SHIFT != 0)
imIo.AddKeyEvent(imgui.ModAlt, e.Keysym.Mod&sdl.KMOD_ALT != 0)
imIo.AddKeyEvent(imgui.ModSuper, e.Keysym.Mod&sdl.KMOD_GUI != 0)
imIo.AddKeyEvent(nmageimgui.SdlScancodeToImGuiKey(e.Keysym.Scancode), e.Type == sdl.KEYDOWN)
case *sdl.TextInputEvent:
imIO.AddInputCharacters(string(e.Text[:]))
imIo.AddInputCharactersUTF8(e.GetText())
case *sdl.MouseButtonEvent:
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:
input.HandleMouseMotionEvent(e)
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED {
w.handleWindowResize()
}
@ -73,17 +99,17 @@ func (w *Window) handleInputs() {
}
}
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.
x, y, _ := sdl.GetMouseState()
imIO.SetMousePosition(imgui.Vec2{X: float32(x), Y: float32(y)})
imIO.SetMouseButtonDown(0, input.MouseDown(sdl.BUTTON_LEFT))
imIO.SetMouseButtonDown(1, input.MouseDown(sdl.BUTTON_RIGHT))
imIO.SetMouseButtonDown(2, input.MouseDown(sdl.BUTTON_MIDDLE))
imIO.KeyShift(sdl.SCANCODE_LSHIFT, sdl.SCANCODE_RSHIFT)
imIO.KeyCtrl(sdl.SCANCODE_LCTRL, sdl.SCANCODE_RCTRL)
imIO.KeyAlt(sdl.SCANCODE_LALT, sdl.SCANCODE_RALT)
imIo.SetMouseButtonDown(imgui.MouseButtonLeft, isSdlButtonLeftDown)
imIo.SetMouseButtonDown(imgui.MouseButtonRight, isSdlButtonRightDown)
imIo.SetMouseButtonDown(imgui.MouseButtonMiddle, isSdlButtonMiddleDown)
}
func (w *Window) handleWindowResize() {
@ -112,7 +138,7 @@ func Init() error {
func initSDL() error {
err := sdl.Init(sdl.INIT_EVERYTHING)
err := sdl.Init(sdl.INIT_TIMER | sdl.INIT_VIDEO)
if err != nil {
return err
}
@ -122,56 +148,66 @@ func initSDL() error {
sdl.GLSetAttribute(sdl.MAJOR_VERSION, 4)
sdl.GLSetAttribute(sdl.MINOR_VERSION, 1)
// R(0-255) G(0-255) B(0-255)
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 CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) {
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags)
}
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
return createWindow(title, -1, -1, width, height, WindowFlags_OPENGL|flags, rend)
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags) (Window, error) {
return createWindow(title, sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, width, height, WindowFlags_OPENGL|flags)
}
func createWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
func createWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) {
assert.T(isInited, "engine.Init was not called!")
if x == -1 && y == -1 {
x = sdl.WINDOWPOS_CENTERED
y = sdl.WINDOWPOS_CENTERED
}
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,
win := Window{
SDLWin: nil,
EventCallbacks: make([]func(sdl.Event), 0),
Rend: rend,
}
win.GlCtx, err = sdlWin.GLCreateContext()
var err error
win.SDLWin, err = sdl.CreateWindow(title, x, y, width, height, uint32(flags))
if err != nil {
return nil, err
return win, err
}
win.GlCtx, err = win.SDLWin.GLCreateContext()
if err != nil {
return win, err
}
err = initOpenGL()
if err != nil {
return nil, err
return win, err
}
setupDefaultTextures()
// 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)
win.SDLWin.GLSwap()
return win, err
}
@ -182,19 +218,82 @@ func initOpenGL() error {
}
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 setupDefaultTextures() error {
// 1x1 black texture
defaultBlackImg := image.NewNRGBA(image.Rect(0, 0, 1, 1))
defaultBlackImg.Set(0, 0, color.NRGBA{R: 0, G: 0, B: 0, A: 1})
defaultBlackImgTex, err := assets.LoadTextureInMemPngImg(defaultBlackImg, &assets.TextureLoadOptions{NoSrgba: true})
if err != nil {
return err
}
assets.DefaultBlackTexId = defaultBlackImgTex
// 1x1 white texture
defaultWhiteImg := image.NewNRGBA(image.Rect(0, 0, 1, 1))
defaultWhiteImg.Set(0, 0, color.NRGBA{R: 255, G: 255, B: 255, A: 1})
defaultWhiteImgTex, err := assets.LoadTextureInMemPngImg(defaultWhiteImg, &assets.TextureLoadOptions{NoSrgba: true})
if err != nil {
return err
}
assets.DefaultWhiteTexId = defaultWhiteImgTex
// Default diffuse
assets.DefaultDiffuseTexId = defaultWhiteImgTex
// Default specular
assets.DefaultSpecularTexId = defaultBlackImgTex
// Default Normal map which is created to be RGB(0.5,0.5,1), which when multiplied by TBN matrix gives the vertex normal.
// 128 is better than 127 for normal maps. See 'Flat Color' section here: http://wiki.polycount.com/wiki/Normal_map
// Basically, 127 can create seams while 128 looks correct
defaultNormalMapImg := image.NewNRGBA(image.Rect(0, 0, 1, 1))
defaultNormalMapImg.Set(0, 0, color.NRGBA{R: 128, G: 128, B: 255, A: 1})
defaultNormalMapTex, err := assets.LoadTextureInMemPngImg(defaultNormalMapImg, &assets.TextureLoadOptions{NoSrgba: true})
if err != nil {
return err
}
assets.DefaultNormalTexId = defaultNormalMapTex
// Default emission
assets.DefaultEmissionTexId = defaultBlackImgTex
assert.T(assets.DefaultBlackTexId.TexID != 0, "The default black texture handle is zero. Either texture wasn't created or handle wasn't updated")
assert.T(assets.DefaultWhiteTexId.TexID != 0, "The default white texture handle is zero. Either texture wasn't created or handle wasn't updated")
assert.T(assets.DefaultDiffuseTexId.TexID != 0, "The default diffuse texture handle is zero. Either texture wasn't created or handle wasn't updated")
assert.T(assets.DefaultSpecularTexId.TexID != 0, "The default specular texture handle is zero. Either texture wasn't created or handle wasn't updated")
assert.T(assets.DefaultNormalTexId.TexID != 0, "The default normal texture handle is zero. Either texture wasn't created or handle wasn't updated")
assert.T(assets.DefaultEmissionTexId.TexID != 0, "The default emission texture handle is zero. Either texture wasn't created or handle wasn't updated")
return nil
}
func SetSrgbFramebuffer(isEnabled bool) {
if isEnabled {
gl.Enable(gl.FRAMEBUFFER_SRGB)
} else {
gl.Disable(gl.FRAMEBUFFER_SRGB)
}
}
func SetVSync(enabled bool) {
assert.T(isInited, "engine.Init was not called!")
if enabled {
sdl.GLSetSwapInterval(1)
@ -202,3 +301,12 @@ func SetVSync(enabled bool) {
sdl.GLSetSwapInterval(0)
}
}
func SetMSAA(isEnabled bool) {
if isEnabled {
gl.Enable(gl.MULTISAMPLE)
} else {
gl.Disable(gl.MULTISAMPLE)
}
}

View File

@ -1,6 +1,7 @@
package engine
import (
"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"
@ -20,22 +21,29 @@ type Game interface {
DeInit()
}
func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
func Run(g Game, w *Window, rend renderer.Render, 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()
//Simulate an imgui frame during init so any imgui calls are allowed within init
tempWidth, tempHeight := w.SDLWin.GetSize()
tempFBWidth, tempFBHeight := w.SDLWin.GLGetDrawableSize()
ui.FrameStart(float32(tempWidth), float32(tempHeight))
ui.Render(float32(tempWidth), float32(tempHeight), tempFBWidth, tempFBHeight)
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()
width, height = w.SDLWin.GetSize()
fbWidth, fbHeight = w.SDLWin.GLGetDrawableSize()
timing.FrameStarted()
w.handleInputs()
@ -43,13 +51,13 @@ func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
g.Update()
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
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()
rend.FrameEnd()
timing.FrameEnded()
}

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

View File

@ -1,77 +1,7 @@
package entity
type EntityFlag byte
import "github.com/bloeys/nmage/registry"
const (
EntityFlag_Unknown EntityFlag = 0
EntityFlag_Dead EntityFlag = 1 << (iota - 1)
EntityFlag_Alive
)
const (
GenerationShiftBits = 64 - 8
FlagsShiftBits = 64 - 16
IndexBitMask = 0x00_00_FFFF_FFFF_FFFF
)
type Entity struct {
// Byte 1: Generation; Byte 2: Flags; Bytes 3-8: Index
ID uint64
Comps []Comp
}
func GetGeneration(id uint64) byte {
return byte(id >> GenerationShiftBits)
}
func GetFlags(id uint64) byte {
return byte(id >> FlagsShiftBits)
}
func GetIndex(id uint64) uint64 {
return id & IndexBitMask
}
func (e *Entity) HasFlag(ef EntityFlag) bool {
return GetFlags(e.ID)&byte(ef) > 0
}
func NewEntityId(generation, flags byte, index uint64) uint64 {
return index | (uint64(generation) << GenerationShiftBits) | (uint64(flags) << FlagsShiftBits)
}
type Comp interface {
Name() string
}
func AddComp(e *Entity, c Comp) {
e.Comps = append(e.Comps, c)
}
func GetComp[T Comp](e *Entity) (out T) {
for i := 0; i < len(e.Comps); i++ {
comp, ok := e.Comps[i].(T)
if ok {
return comp
}
}
return out
}
func GetAllCompOfType[T Comp](e *Entity) (out []T) {
out = []T{}
for i := 0; i < len(e.Comps); i++ {
comp, ok := e.Comps[i].(T)
if ok {
out = append(out, comp)
}
}
return out
type Entity interface {
GetHandle() registry.Handle
}

View File

@ -1,94 +0,0 @@
package entity
import (
"github.com/bloeys/nmage/assert"
)
type freeListitem struct {
EntityIndex uint64
nextFree *freeListitem
}
type Registry struct {
EntityCount uint64
Entities []Entity
FreeList *freeListitem
}
func (r *Registry) NewEntity() *Entity {
assert.T(r.EntityCount < uint64(len(r.Entities)), "Can not add more entities to registry because it is full")
entityToUseIndex := uint64(0)
var entityToUse *Entity = nil
if r.FreeList != nil {
entityToUseIndex = r.FreeList.EntityIndex
entityToUse = &r.Entities[entityToUseIndex]
r.FreeList = r.FreeList.nextFree
} else {
for i := 0; i < len(r.Entities); i++ {
e := &r.Entities[i]
if GetFlags(e.ID) != byte(EntityFlag_Unknown) && !e.HasFlag(EntityFlag_Dead) {
continue
}
entityToUse = e
entityToUseIndex = uint64(i)
break
}
}
if entityToUse == nil {
panic("failed to create new entity because we did not find a free spot in the registry. Why did the assert not go off?")
}
r.EntityCount++
entityToUse.ID = NewEntityId(GetGeneration(entityToUse.ID)+1, byte(EntityFlag_Alive), entityToUseIndex)
assert.T(entityToUse.ID != 0, "Entity ID must not be zero")
return entityToUse
}
func (r *Registry) GetEntity(id uint64) *Entity {
index := GetIndex(id)
gen := GetGeneration(id)
e := &r.Entities[index]
eGen := GetGeneration(e.ID)
if gen != eGen {
return nil
}
return e
}
func (r *Registry) FreeEntity(id uint64) {
e := r.GetEntity(id)
if e == nil {
return
}
r.EntityCount--
eIndex := GetIndex(e.ID)
e.Comps = []Comp{}
e.ID = NewEntityId(GetGeneration(e.ID), byte(EntityFlag_Dead), eIndex)
r.FreeList = &freeListitem{
EntityIndex: eIndex,
nextFree: r.FreeList,
}
}
func NewRegistry(size uint32) *Registry {
assert.T(size > 0, "Registry size must be more than zero")
return &Registry{
Entities: make([]Entity, size),
}
}

14
go.mod
View File

@ -1,13 +1,19 @@
module github.com/bloeys/nmage
go 1.18
go 1.22
require github.com/veandco/go-sdl2 v0.4.25
require github.com/veandco/go-sdl2 v0.4.35
require github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6
require (
github.com/bloeys/assimp-go v0.4.4
github.com/bloeys/gglm v0.42.0
github.com/inkyblackness/imgui-go/v4 v4.6.0
github.com/bloeys/gglm v0.49.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

49
go.sum
View File

@ -1,17 +1,40 @@
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.42.0 h1:UAUFGTaZv3dpZ0YSIQVum3bdeCZgNmx965VLnD2v11k=
github.com/bloeys/gglm v0.42.0/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/bloeys/gglm v0.49.0 h1:YtbyHpszYhjnxw7KVV0LaCdBktRMqfGx/i37EMomxsE=
github.com/bloeys/gglm v0.49.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/inkyblackness/imgui-go/v4 v4.6.0 h1:ShcnXEYl80+xREGBY9OpGWePA6FfJChY9Varsm+3jjE=
github.com/inkyblackness/imgui-go/v4 v4.6.0/go.mod h1:g8SAGtOYUP7rYaOB2AsVKCEHmPMDmJKgt4z6d+flhb0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/veandco/go-sdl2 v0.4.25 h1:J5ac3KKOccp/0xGJA1PaNYKPUcZm19IxhDGs8lJofPI=
github.com/veandco/go-sdl2 v0.4.25/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
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

@ -1,6 +1,23 @@
// The input package provides an interface to mouse and keyboard inputs
// like key clicks and releases, along with some higher level constructs like
// pressed/released this frames, double clicks, and normalized inputs.
//
// The input package has two sets of functions for most cases, where one
// is in the form 'xy' and the other 'xyCaptured'. The captured form
// always returns normal events even if the mouse or keyboard are captured
// by the UI system. The 'xy' form however will return zero/false if the
// respective input device is currently captured (with the exception of mouse position, that is always correctly returned).
//
// For most cases, you want to use the 'xy' form. For example, you only want to receive
// key down events for game character movement when the UI isn't capturing the keyboard,
// because otherwise the character will move while typing in a UI textbox.
//
// The functions IsMouseCaptured and IsKeyboardCaptured are also available.
package input
import "github.com/veandco/go-sdl2/sdl"
import (
"github.com/veandco/go-sdl2/sdl"
)
type keyState struct {
Key sdl.Keycode
@ -31,24 +48,33 @@ type mouseWheelState struct {
}
var (
keyMap = make(map[sdl.Keycode]*keyState)
mouseBtnMap = make(map[int]*mouseBtnState)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
quitRequested bool
mouseWheel = mouseWheelState{}
mouseMotion = mouseMotionState{}
mouseBtnMap = make(map[int]mouseBtnState)
keyMap = make(map[sdl.Keycode]keyState)
isQuitRequested bool
isMouseCaptured bool
isKeyboardCaptured bool
)
func EventLoopStart() {
func EventLoopStart(mouseGotCaptured, keyboardGotCaptured bool) {
for _, v := range keyMap {
isMouseCaptured = mouseGotCaptured
isKeyboardCaptured = keyboardGotCaptured
// Update per-frame state
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
@ -57,42 +83,62 @@ func EventLoopStart() {
mouseWheel.XDelta = 0
mouseWheel.YDelta = 0
quitRequested = false
isQuitRequested = false
}
func ClearKeyboardState() {
clear(keyMap)
}
func ClearMouseState() {
clear(mouseBtnMap)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
}
func HandleQuitEvent(e *sdl.QuitEvent) {
quitRequested = true
isQuitRequested = true
}
func IsMouseCaptured() bool {
return isMouseCaptured
}
func IsKeyboardCaptured() bool {
return isKeyboardCaptured
}
func IsQuitClicked() bool {
return quitRequested
return isQuitRequested
}
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) {
@ -109,18 +155,36 @@ 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 regardless of whether the mouse is captured or not
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) {
if isMouseCaptured {
return 0, 0
}
return GetMouseMotionCaptured()
}
func GetMouseMotionCaptured() (xDelta, yDelta int32) {
return mouseMotion.XDelta, mouseMotion.YDelta
}
func GetMouseMotionNorm() (xDelta, yDelta int32) {
if isMouseCaptured {
return 0, 0
}
return GetMouseMotionNormCaptured()
}
func GetMouseMotionNormCaptured() (xDelta, yDelta int32) {
x, y := mouseMotion.XDelta, mouseMotion.YDelta
if x > 0 {
x = 1
@ -138,12 +202,31 @@ func GetMouseMotionNorm() (xDelta, yDelta int32) {
}
func GetMouseWheelMotion() (xDelta, yDelta int32) {
if isMouseCaptured {
return 0, 0
}
return GetMouseWheelMotionCaptured()
}
func GetMouseWheelMotionCaptured() (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 isMouseCaptured {
return 0
}
return GetMouseWheelXNormCaptured()
}
// GetMouseWheelXNormCaptured returns 1 if mouse wheel xDelta > 0, -1 if xDelta < 0, and 0 otherwise
func GetMouseWheelXNormCaptured() int32 {
if mouseWheel.XDelta > 0 {
return 1
} else if mouseWheel.XDelta < 0 {
@ -153,9 +236,19 @@ func GetMouseWheelXNorm() int32 {
return 0
}
//returns 1 if mouse wheel yDelta > 0, -1 if yDelta < 0, and 0 otherwise
// GetMouseWheelYNorm returns 1 if mouse wheel yDelta > 0, -1 if yDelta < 0, and 0 otherwise
func GetMouseWheelYNorm() int32 {
if isMouseCaptured {
return 0
}
return GetMouseWheelYNormCaptured()
}
// GetMouseWheelYNormCaptured returns 1 if mouse wheel yDelta > 0, -1 if yDelta < 0, and 0 otherwise
func GetMouseWheelYNormCaptured() int32 {
if mouseWheel.YDelta > 0 {
return 1
} else if mouseWheel.YDelta < 0 {
@ -167,8 +260,17 @@ func GetMouseWheelYNorm() int32 {
func KeyClicked(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
if isKeyboardCaptured {
return false
}
return KeyClickedCaptured(kc)
}
func KeyClickedCaptured(kc sdl.Keycode) bool {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -177,8 +279,17 @@ func KeyClicked(kc sdl.Keycode) bool {
func KeyReleased(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
if isKeyboardCaptured {
return false
}
return KeyReleasedCaptured(kc)
}
func KeyReleasedCaptured(kc sdl.Keycode) bool {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -187,8 +298,17 @@ func KeyReleased(kc sdl.Keycode) bool {
func KeyDown(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
if isKeyboardCaptured {
return false
}
return KeyDownCaptured(kc)
}
func KeyDownCaptured(kc sdl.Keycode) bool {
ks, ok := keyMap[kc]
if !ok {
return false
}
@ -197,8 +317,17 @@ func KeyDown(kc sdl.Keycode) bool {
func KeyUp(kc sdl.Keycode) bool {
ks := keyMap[kc]
if ks == nil {
if isKeyboardCaptured {
return false
}
return KeyUpCaptured(kc)
}
func KeyUpCaptured(kc sdl.Keycode) bool {
ks, ok := keyMap[kc]
if !ok {
return true
}
@ -207,8 +336,17 @@ func KeyUp(kc sdl.Keycode) bool {
func MouseClicked(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
if isMouseCaptured {
return false
}
return MouseClickedCaptued(mb)
}
func MouseClickedCaptued(mb int) bool {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -217,8 +355,17 @@ func MouseClicked(mb int) bool {
func MouseDoubleClicked(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
if isMouseCaptured {
return false
}
return MouseDoubleClickedCaptured(mb)
}
func MouseDoubleClickedCaptured(mb int) bool {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -226,8 +373,18 @@ func MouseDoubleClicked(mb int) bool {
}
func MouseReleased(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
if isMouseCaptured {
return false
}
return MouseReleasedCaptured(mb)
}
func MouseReleasedCaptured(mb int) bool {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -236,8 +393,17 @@ func MouseReleased(mb int) bool {
func MouseDown(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
if isMouseCaptured {
return false
}
return MouseDownCaptued(mb)
}
func MouseDownCaptued(mb int) bool {
btn, ok := mouseBtnMap[mb]
if !ok {
return false
}
@ -246,8 +412,17 @@ func MouseDown(mb int) bool {
func MouseUp(mb int) bool {
btn := mouseBtnMap[mb]
if btn == nil {
if isMouseCaptured {
return false
}
return MouseUpCaptured(mb)
}
func MouseUpCaptured(mb int) bool {
btn, ok := mouseBtnMap[mb]
if !ok {
return true
}

View File

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

1424
main.go

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,127 @@
package materials
import (
_ "unsafe"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/assets"
"github.com/bloeys/nmage/logging"
"github.com/bloeys/nmage/shaders"
"github.com/go-gl/gl/v4.1-core/gl"
)
// @TODO: This noescape magic is to avoid heap allocations done when
// passing vectors or matrices into cgo via set uniform calls.
//
// But I would rather this kind of stuff is done on the gl wrapper level.
// Should we wrap the OpenGL APIs we use ourself?
var (
lastMatId uint32
)
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_Cubemap_Array TextureSlot = 11
TextureSlot_ShadowMap1 TextureSlot = 12
TextureSlot_ShadowMap_Array1 TextureSlot = 13
)
type MaterialSettings uint64
const (
MaterialSettings_None MaterialSettings = iota
MaterialSettings_HasModelMtx MaterialSettings = 1 << (iota - 1)
MaterialSettings_HasNormalMtx
)
func (ms *MaterialSettings) Set(flags MaterialSettings) {
*ms |= flags
}
func (ms *MaterialSettings) Remove(flags MaterialSettings) {
*ms &= ^flags
}
func (ms *MaterialSettings) Has(flags MaterialSettings) bool {
return *ms&flags == flags
}
type Material struct {
Id uint32
Name string
ShaderProg shaders.ShaderProgram
DiffuseTex uint32
Settings MaterialSettings
UnifLocs map[string]int32
AttribLocs map[string]int32
// @TODO: Do this in a better way?. Perhaps something like how we do fbo attachments? Or keep it?
// Phong shading
DiffuseTex uint32
SpecularTex uint32
NormalTex uint32
EmissionTex uint32
// Shininess of specular highlights
Shininess float32
// Cubemaps
CubemapTex uint32
CubemapArrayTex uint32
// Shadowmaps
ShadowMapTex1 uint32
ShadowMapTexArray1 uint32
}
func (m *Material) Bind() {
gl.UseProgram(m.ShaderProg.ID)
m.ShaderProg.Bind()
gl.ActiveTexture(gl.TEXTURE0)
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Diffuse))
gl.BindTexture(gl.TEXTURE_2D, m.DiffuseTex)
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Specular))
gl.BindTexture(gl.TEXTURE_2D, m.SpecularTex)
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Normal))
gl.BindTexture(gl.TEXTURE_2D, m.NormalTex)
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_Emission))
gl.BindTexture(gl.TEXTURE_2D, m.EmissionTex)
// @TODO: Have defaults for these
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_ShadowMap1))
gl.BindTexture(gl.TEXTURE_2D, m.ShadowMapTex1)
}
if m.ShadowMapTexArray1 != 0 {
gl.ActiveTexture(uint32(gl.TEXTURE0 + TextureSlot_ShadowMap_Array1))
gl.BindTexture(gl.TEXTURE_2D_ARRAY, m.ShadowMapTexArray1)
}
}
func (m *Material) UnBind() {
gl.UseProgram(0)
//TODO: Should we unbind textures here? Are these two lines needed?
// gl.ActiveTexture(gl.TEXTURE0)
// gl.BindTexture(gl.TEXTURE_2D, 0)
}
func (m *Material) GetAttribLoc(attribName string) int32 {
@ -41,7 +131,7 @@ func (m *Material) GetAttribLoc(attribName string) int32 {
return loc
}
loc = gl.GetAttribLocation(m.ShaderProg.ID, gl.Str(attribName+"\x00"))
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
@ -54,7 +144,7 @@ func (m *Material) GetUnifLoc(uniformName string) int32 {
return loc
}
loc = gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
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
@ -69,57 +159,132 @@ func (m *Material) DisableAttribute(attribName string) {
}
func (m *Material) SetUnifInt32(uniformName string, val int32) {
gl.ProgramUniform1i(m.ShaderProg.ID, m.GetUnifLoc(uniformName), val)
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)
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])
internalSetUnifVec2(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec2)
}
//go:noescape
//go:linkname internalSetUnifVec2 github.com/bloeys/nmage/materials.SetUnifVec2
func internalSetUnifVec2(shaderProgId uint32, unifLoc int32, vec2 *gglm.Vec2)
func SetUnifVec2(shaderProgId uint32, unifLoc int32, vec2 *gglm.Vec2) {
gl.ProgramUniform2fv(shaderProgId, unifLoc, 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])
internalSetUnifVec3(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec3)
}
//go:noescape
//go:linkname internalSetUnifVec3 github.com/bloeys/nmage/materials.SetUnifVec3
func internalSetUnifVec3(shaderProgId uint32, unifLoc int32, vec3 *gglm.Vec3)
func SetUnifVec3(shaderProgId uint32, unifLoc int32, vec3 *gglm.Vec3) {
gl.ProgramUniform3fv(shaderProgId, unifLoc, 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])
internalSetUnifVec4(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec4)
}
//go:noescape
//go:linkname internalSetUnifVec4 github.com/bloeys/nmage/materials.SetUnifVec4
func internalSetUnifVec4(shaderProgId uint32, unifLoc int32, vec4 *gglm.Vec4)
func SetUnifVec4(shaderProgId uint32, unifLoc int32, vec4 *gglm.Vec4) {
gl.ProgramUniform4fv(shaderProgId, unifLoc, 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])
internalSetUnifMat2(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat2)
}
//go:noescape
//go:linkname internalSetUnifMat2 github.com/bloeys/nmage/materials.SetUnifMat2
func internalSetUnifMat2(shaderProgId uint32, unifLoc int32, mat2 *gglm.Mat2)
func SetUnifMat2(shaderProgId uint32, unifLoc int32, mat2 *gglm.Mat2) {
gl.ProgramUniformMatrix2fv(shaderProgId, unifLoc, 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])
internalSetUnifMat3(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat3)
}
//go:noescape
//go:linkname internalSetUnifMat3 github.com/bloeys/nmage/materials.SetUnifMat3
func internalSetUnifMat3(shaderProgId uint32, unifLoc int32, mat3 *gglm.Mat3)
func SetUnifMat3(shaderProgId uint32, unifLoc int32, mat3 *gglm.Mat3) {
gl.ProgramUniformMatrix3fv(shaderProgId, unifLoc, 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])
internalSetUnifMat4(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat4)
}
//go:noescape
//go:linkname internalSetUnifMat4 github.com/bloeys/nmage/materials.SetUnifMat4
func internalSetUnifMat4(shaderProgId uint32, unifLoc int32, mat4 *gglm.Mat4)
func SetUnifMat4(shaderProgId uint32, unifLoc int32, mat4 *gglm.Mat4) {
gl.ProgramUniformMatrix4fv(shaderProgId, unifLoc, 1, false, &mat4.Data[0][0])
}
func (m *Material) Delete() {
gl.DeleteProgram(m.ShaderProg.ID)
gl.DeleteProgram(m.ShaderProg.Id)
}
func NewMaterial(matName, shaderPath string) *Material {
func getNewMatId() uint32 {
lastMatId++
return lastMatId
}
func NewMaterial(matName, shaderPath string) Material {
shdrProg, err := shaders.LoadAndCompileCombinedShader(shaderPath)
if err != nil {
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
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)}
return Material{
Id: getNewMatId(),
Name: matName,
ShaderProg: shdrProg,
UnifLocs: make(map[string]int32),
AttribLocs: make(map[string]int32),
DiffuseTex: assets.DefaultDiffuseTexId.TexID,
SpecularTex: assets.DefaultSpecularTexId.TexID,
NormalTex: assets.DefaultNormalTexId.TexID,
EmissionTex: assets.DefaultEmissionTexId.TexID,
}
}
func NewMaterialSrc(matName string, shaderSrc []byte) *Material {
func NewMaterialSrc(matName string, shaderSrc []byte) Material {
shdrProg, err := shaders.LoadAndCompileCombinedShaderSrc(shaderSrc)
if err != nil {
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
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)}
return Material{
Id: getNewMatId(),
Name: matName,
ShaderProg: shdrProg,
UnifLocs: make(map[string]int32),
AttribLocs: make(map[string]int32),
DiffuseTex: assets.DefaultDiffuseTexId.TexID,
SpecularTex: assets.DefaultSpecularTexId.TexID,
NormalTex: assets.DefaultNormalTexId.TexID,
EmissionTex: assets.DefaultEmissionTexId.TexID,
}
}

View File

@ -2,7 +2,6 @@ package meshes
import (
"errors"
"fmt"
"github.com/bloeys/assimp-go/asig"
"github.com/bloeys/gglm/gglm"
@ -10,49 +9,158 @@ import (
"github.com/bloeys/nmage/buffers"
)
type Mesh struct {
Name string
Buf buffers.Buffer
type SubMesh struct {
BaseVertex int32
BaseIndex uint32
IndexCount int32
}
func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh, error) {
type Mesh struct {
Name string
/*
Vao has the following shader attribute layout:
- Loc0: Pos
- Loc1: Normal
- Loc2: UV0
- Loc3: Tangent
- (Optional) Color
scene, release, err := asig.ImportFile(modelPath, asig.PostProcessTriangulate|postProcessFlags)
Optional stuff appear in the order in this list, depending on what other optional stuff exists.
For example:
- If color exists it will be in Loc3, otherwise it is unset
*/
Vao buffers.VertexArray
SubMeshes []SubMesh
}
var (
// DefaultMeshLoadFlags are the flags always applied when loading a new mesh regardless
// of what post process flags are used when loading a mesh.
//
// Defaults to: asig.PostProcessTriangulate | asig.PostProcessCalcTangentSpace;
// Note: changing this will break the normal lit shaders, which expect tangents to be there
DefaultMeshLoadFlags asig.PostProcess = asig.PostProcessTriangulate | asig.PostProcessCalcTangentSpace
)
func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (Mesh, error) {
finalPostProcessFlags := DefaultMeshLoadFlags | postProcessFlags
scene, release, err := asig.ImportFile(modelPath, finalPostProcessFlags)
if err != nil {
return nil, errors.New("Failed to load model. Err: " + err.Error())
return Mesh{}, 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)
return Mesh{}, errors.New("No meshes found in file: " + modelPath)
}
mesh := &Mesh{Name: name}
sceneMesh := scene.Meshes[0]
mesh.Buf = buffers.NewBuffer()
if len(sceneMesh.TexCoords[0]) == 0 {
sceneMesh.TexCoords[0] = make([]gglm.Vec3, len(sceneMesh.Vertices))
mesh := Mesh{
Name: name,
Vao: buffers.NewVertexArray(),
SubMeshes: make([]SubMesh, 0, 1),
}
layoutToUse := []buffers.Element{{ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec2}}
vbo := buffers.NewVertexBuffer()
ibo := buffers.NewIndexBuffer()
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
layoutToUse = append(layoutToUse, buffers.Element{ElementType: buffers.DataTypeVec4})
}
mesh.Buf.SetLayout(layoutToUse...)
// Estimate a useful prealloc capacity based on the first submesh that has vertex pos+normals+tangents+texCoords
vertexBufDataCapacity := len(scene.Meshes[0].Vertices) * 3 * 3 * 3 * 2
var values []float32
arrs := []arrToInterleave{{V3s: sceneMesh.Vertices}, {V3s: sceneMesh.Normals}, {V2s: v3sToV2s(sceneMesh.TexCoords[0])}}
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
arrs = append(arrs, arrToInterleave{V4s: sceneMesh.ColorSets[0]})
// Increase capacity depending on what the mesh has
if len(scene.Meshes[0].ColorSets) > 0 && len(scene.Meshes[0].ColorSets[0]) > 0 {
vertexBufDataCapacity *= 4
}
values = interleave(arrs...)
var vertexBufData []float32 = make([]float32, 0, vertexBufDataCapacity)
// Initial size assumes 3 indices per face
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]
// We always want tangents and UV0
if len(sceneMesh.Tangents) == 0 {
sceneMesh.Tangents = make([]gglm.Vec3, len(sceneMesh.Vertices))
}
if len(sceneMesh.TexCoords[0]) == 0 {
sceneMesh.TexCoords[0] = make([]gglm.Vec3, len(sceneMesh.Vertices))
}
hasColorSet0 := len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0
layoutToUse := []buffers.Element{
{ElementType: buffers.DataTypeVec3}, // Position
{ElementType: buffers.DataTypeVec3}, // Normals
{ElementType: buffers.DataTypeVec3}, // Tangents
{ElementType: buffers.DataTypeVec2}, // UV0
}
if hasColorSet0 {
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},
{V3s: sceneMesh.Tangents},
{V2s: v3sToV2s(sceneMesh.TexCoords[0])},
}
if hasColorSet0 {
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()
mesh.Buf.SetData(values)
mesh.Buf.SetIndexBufData(flattenFaces(sceneMesh.Faces))
return mesh, nil
}
@ -76,9 +184,9 @@ type arrToInterleave struct {
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 both arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
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[:]
@ -130,7 +238,7 @@ func interleave(arrs ...arrToInterleave) []float32 {
func flattenFaces(faces []asig.Face) []uint32 {
assert.T(len(faces[0].Indices) == 3, fmt.Sprintf("Face doesn't have 3 indices. Index count: %v\n", len(faces[0].Indices)))
assert.T(len(faces[0].Indices) == 3, "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++ {

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

View File

@ -2,6 +2,7 @@ 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"
@ -11,29 +12,73 @@ import (
var _ renderer.Render = &Rend3DGL{}
type Rend3DGL struct {
BoundMesh *meshes.Mesh
BoundMat *materials.Material
BoundVaoId uint32
BoundMatId uint32
BoundMeshVaoId uint32
}
func (r3d *Rend3DGL) Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material) {
func (r *Rend3DGL) DrawMesh(mesh *meshes.Mesh, modelMat *gglm.TrMat, mat *materials.Material) {
if mesh != r3d.BoundMesh {
mesh.Buf.Bind()
r3d.BoundMesh = mesh
if mesh.Vao.Id != r.BoundMeshVaoId {
mesh.Vao.Bind()
r.BoundMeshVaoId = mesh.Vao.Id
}
if mat != r3d.BoundMat {
if mat.Id != r.BoundMatId {
mat.Bind()
r3d.BoundMat = mat
r.BoundMatId = mat.Id
}
mat.SetUnifMat4("modelMat", &trMat.Mat4)
gl.DrawElements(gl.TRIANGLES, mesh.Buf.IndexBufCount, gl.UNSIGNED_INT, gl.PtrOffset(0))
if mat.Settings.Has(materials.MaterialSettings_HasModelMtx) {
mat.SetUnifMat4("modelMat", &modelMat.Mat4)
}
if mat.Settings.Has(materials.MaterialSettings_HasNormalMtx) {
normalMat := modelMat.Clone().InvertAndTranspose().ToMat3()
mat.SetUnifMat3("normalMat", &normalMat)
}
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.Id != r.BoundVaoId {
vao.Bind()
r.BoundVaoId = vao.Id
}
if mat.Id != r.BoundMatId {
mat.Bind()
r.BoundMatId = mat.Id
}
gl.DrawArrays(gl.TRIANGLES, firstElement, elementCount)
}
func (r *Rend3DGL) DrawCubemap(mesh *meshes.Mesh, mat *materials.Material) {
if mesh.Vao.Id != r.BoundMeshVaoId {
mesh.Vao.Bind()
r.BoundMeshVaoId = mesh.Vao.Id
}
if mat.Id != r.BoundMatId {
mat.Bind()
r.BoundMatId = mat.Id
}
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
r3d.BoundVaoId = 0
r3d.BoundMatId = 0
r3d.BoundMeshVaoId = 0
}
func NewRend3DGL() *Rend3DGL {

View File

@ -2,11 +2,14 @@ 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 {
Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material)
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()
}

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.

View File

@ -0,0 +1,54 @@
//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;
#define NUM_PROJ_VIEW_MATS 4
// 3 * NUM_PROJ_VIEW_MATS
layout (triangle_strip, max_vertices=12) out;
// This is the same number as max spot lights or whatever else is being rendered
uniform mat4 projViewMats[NUM_PROJ_VIEW_MATS];
out vec4 FragPos;
void main()
{
for(int projViewMatIndex = 0; projViewMatIndex < NUM_PROJ_VIEW_MATS; projViewMatIndex++){
gl_Layer = projViewMatIndex;
mat4 projViewMat = projViewMats[projViewMatIndex];
for(int i = 0; i < 3; i++)
{
FragPos = gl_in[i].gl_Position;
gl_Position = projViewMat * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
//shader:fragment
#version 410
in vec4 FragPos;
void main()
{
// This implicitly writes to the depth buffer with no color operations
// Equivalent: gl_FragDepth = gl_FragCoord.z;
}

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

@ -0,0 +1,49 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
layout(location=2) in vec3 vertTangentIn;
layout(location=3) in vec2 vertUV0In;
layout(location=4) in vec3 vertColorIn;
out vec2 vertUV0;
out vec3 vertColor;
out vec3 fragPos;
uniform mat4 modelMat;
uniform mat4 projViewMat;
void main()
{
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 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);
}

21
res/shaders/depth-map.glsl Executable file
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;
}

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

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

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

@ -0,0 +1,46 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
layout(location=1) in vec3 vertNormalIn;
layout(location=2) in vec3 vertTangentIn;
layout(location=3) in vec2 vertUV0In;
layout(location=4) in vec3 vertColorIn;
out vec2 vertUV0;
out vec3 vertColor;
out vec3 fragPos;
uniform mat4 modelMat;
uniform mat4 projViewMat;
void main()
{
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 vec2 vertUV0;
in vec3 fragPos;
out vec4 fragColor;
void main()
{
vec4 diffuseTexColor = texture(material.diffuse, vertUV0);
fragColor = vec4(diffuseTexColor.rgb, 1);
}

View File

@ -1,55 +1,415 @@
//shader:vertex
#version 410
#define NUM_SPOT_LIGHTS 4
#define NUM_POINT_LIGHTS 8
//
// Inputs
//
layout(location=0) in vec3 vertPosIn;
layout(location=1) in vec3 vertNormalIn;
layout(location=2) in vec2 vertUV0In;
layout(location=3) in vec3 vertColorIn;
layout(location=2) in vec3 vertTangentIn;
layout(location=3) in vec2 vertUV0In;
layout(location=4) in vec3 vertColorIn;
out vec3 vertNormal;
//
// Uniforms
//
uniform vec3 camPos;
uniform mat4 modelMat;
uniform mat3 normalMat;
uniform mat4 projViewMat;
uniform mat4 dirLightProjViewMat;
uniform mat4 spotLightProjViewMats[NUM_SPOT_LIGHTS];
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;
};
uniform PointLight pointLights[NUM_POINT_LIGHTS];
struct SpotLight {
vec3 pos;
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
float innerCutoff;
float outerCutoff;
};
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
//
// Outputs
//
out vec2 vertUV0;
out vec3 vertColor;
out vec3 fragPos;
//MVP = Model View Projection
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
out vec3 fragPos;
out vec3 fragPosDirLight;
out vec4 fragPosSpotLight[NUM_SPOT_LIGHTS];
out vec3 tangentCamPos;
out vec3 tangentFragPos;
out vec3 tangentDirLightDir;
out vec3 tangentSpotLightPositions[NUM_SPOT_LIGHTS];
out vec3 tangentSpotLightDirections[NUM_SPOT_LIGHTS];
out vec3 tangentPointLightPositions[NUM_POINT_LIGHTS];
void main()
{
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
vertUV0 = vertUV0In;
vertColor = vertColorIn;
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
vec4 modelVert = modelMat * vec4(vertPosIn, 1);
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
// Tangent-BiTangent-Normal matrix for normal mapping
vec3 T = normalize(vec3(modelMat * vec4(vertTangentIn, 0.0)));
vec3 N = normalize(vec3(modelMat * vec4(vertNormalIn, 0.0)));
// Ensure T is orthogonal with respect to N
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
mat3 tbnMtx = transpose(mat3(T, B, N));
// Lighting related
fragPos = modelVert.xyz;
fragPosDirLight = vec3(dirLightProjViewMat * vec4(fragPos, 1));
tangentCamPos = tbnMtx * camPos;
tangentFragPos = tbnMtx * fragPos;
tangentDirLightDir = tbnMtx * dirLight.dir;
for (int i = 0; i < NUM_POINT_LIGHTS; i++)
tangentPointLightPositions[i] = tbnMtx * pointLights[i].pos;
for (int i = 0; i < NUM_SPOT_LIGHTS; i++)
{
fragPosSpotLight[i] = spotLightProjViewMats[i] * vec4(fragPos, 1);
tangentSpotLightPositions[i] = tbnMtx * spotLights[i].pos;
tangentSpotLightDirections[i] = tbnMtx * spotLights[i].dir;
}
gl_Position = projViewMat * modelVert;
}
//shader:fragment
#version 410
uniform float ambientStrength = 0.1;
uniform vec3 ambientLightColor = vec3(1, 1, 1);
/*
Note that while all lighting calculations are done in tangent space,
shadow mapping is done in world space.
uniform vec3 lightPos1;
uniform vec3 lightColor1;
The exception is the bias calculation. Since the bias relies on the normal
and the normal is in tangent space, we use a tangent space fragment position
with it, but the rest of shadow processing is in world space.
*/
uniform sampler2D diffTex;
#define NUM_SPOT_LIGHTS 4
#define NUM_POINT_LIGHTS 8
in vec3 vertColor;
in vec3 vertNormal;
in vec2 vertUV0;
//
// Inputs
//
in vec3 fragPos;
in vec2 vertUV0;
in vec3 vertColor;
in vec3 fragPosDirLight;
in vec4 fragPosSpotLight[NUM_SPOT_LIGHTS];
in vec3 tangentCamPos;
in vec3 tangentFragPos;
in vec3 tangentDirLightDir;
in vec3 tangentSpotLightPositions[NUM_SPOT_LIGHTS];
in vec3 tangentSpotLightDirections[NUM_SPOT_LIGHTS];
in vec3 tangentPointLightPositions[NUM_POINT_LIGHTS];
//
// Uniforms
//
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;
};
uniform PointLight pointLights[NUM_POINT_LIGHTS];
uniform samplerCubeArray pointLightCubeShadowMaps;
struct SpotLight {
vec3 pos;
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
float innerCutoff;
float outerCutoff;
};
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
uniform sampler2DArray spotLightShadowMaps;
uniform vec3 ambientColor = vec3(0.2, 0.2, 0.2);
//
// Outputs
//
out vec4 fragColor;
//
// Global variables used as cache for lighting calculations
//
vec3 tangentViewDir;
vec4 diffuseTexColor;
vec4 specularTexColor;
vec4 emissionTexColor;
vec3 normalizedVertNorm;
float CalcDirShadow(sampler2D shadowMap, vec3 tangentLightDir)
{
// Move from [-1,1] to [0, 1]
vec3 projCoords = fragPosDirLight * 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, tangentLightDir)), 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(-tangentDirLightDir);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, lightDir));
vec3 finalDiffuse = diffuseAmount * dirLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(lightDir + tangentViewDir);
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 worldLightPos, vec3 tangentLightDir, float farPlane) {
vec3 lightToFrag = fragPos - worldLightPos;
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, tangentLightDir)), 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 tangentLightPos = tangentPointLightPositions[lightIndex];
vec3 tangentLightDir = normalize(tangentLightPos - tangentFragPos);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, tangentLightDir));
vec3 finalDiffuse = diffuseAmount * pointLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(tangentLightDir + tangentViewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * pointLight.specularColor * specularTexColor.rgb;
// Attenuation
float distToLight = length(tangentLightPos - tangentFragPos);
float attenuation = 1 / (pointLight.constant + pointLight.linear * distToLight + pointLight.quadratic * (distToLight * distToLight));
// Shadow
float shadow = CalcPointShadow(lightIndex, pointLight.pos, tangentLightDir, pointLight.farPlane);
return (finalDiffuse + finalSpecular) * attenuation * (1 - shadow);
}
float CalcSpotShadow(vec3 tangentLightDir, int lightIndex)
{
// Move from clip space to NDC
vec3 projCoords = fragPosSpotLight[lightIndex].xyz / fragPosSpotLight[lightIndex].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, tangentLightDir)), 0.005);
// 'Percentage Close Filtering'.
// Basically get soft shadows by averaging this texel and surrounding ones
float shadow = 0;
vec2 texelSize = 1 / textureSize(spotLightShadowMaps, 0).xy;
for(int x = -1; x <= 1; x++)
{
for(int y = -1; y <= 1; y++)
{
float pcfDepth = texture(spotLightShadowMaps, vec3(projCoords.xy + vec2(x, y) * texelSize, lightIndex)).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 CalcSpotLight(SpotLight light, int lightIndex)
{
if (light.innerCutoff == 0)
return vec3(0);
vec3 tangentLightDir = tangentSpotLightDirections[lightIndex];
vec3 fragToLightDir = normalize(tangentSpotLightPositions[lightIndex] - tangentFragPos);
// 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(-tangentLightDir));
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 + tangentViewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * light.specularColor * specularTexColor.rgb;
// Shadow
float shadow = CalcSpotShadow(fragToLightDir, lightIndex);
return (finalDiffuse + finalSpecular) * intensity * (1 - shadow);
}
#define DRAW_NORMALS false
void main()
{
vec3 lightDir = normalize(lightPos1 - fragPos);
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
// Shared values
tangentViewDir = normalize(tangentCamPos - tangentFragPos);
diffuseTexColor = texture(material.diffuse, vertUV0);
specularTexColor = texture(material.specular, vertUV0);
emissionTexColor = texture(material.emission, vertUV0);
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
vec4 texColor = texture(diffTex, vertUV0);
fragColor = vec4(texColor.rgb * vertColor * (finalAmbientColor + diffStrength*lightColor1) , texColor.a);
}
// Read normal data encoded [0,1]
normalizedVertNorm = texture(material.normal, vertUV0).rgb;
// Remap normal to [-1,1]
normalizedVertNorm = normalize(normalizedVertNorm * 2.0 - 1.0);
// 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], i);
}
vec3 finalEmission = emissionTexColor.rgb;
vec3 finalAmbient = ambientColor * diffuseTexColor.rgb;
fragColor = vec4(finalColor + finalAmbient + finalEmission, 1);
if (DRAW_NORMALS)
{
fragColor = vec4(texture(material.normal, vertUV0).rgb, 1);
}
}

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

@ -0,0 +1,33 @@
//shader:vertex
#version 410
layout(location=0) in vec3 vertPosIn;
layout(location=1) in vec3 vertNormalIn;
layout(location=2) in vec3 vertTangentIn;
layout(location=3) in vec2 vertUV0In;
layout(location=4) 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);
}

View File

@ -0,0 +1,50 @@
//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)
);
void main()
{
vec4 vertData = quadData[gl_VertexID];
vertUV0 = vertData.zw;
gl_Position = vec4(vertData.xy, 0.0, 1.0);
}
//shader:fragment
#version 410
struct Material {
sampler2D diffuse;
};
uniform float exposure = 1;
uniform Material material;
in vec2 vertUV0;
out vec4 fragColor;
void main()
{
vec4 diffuseTexColor = texture(material.diffuse, vertUV0);
// Reinhard tone mapping
// vec3 mappedColor = diffuseTexColor.rgb / (diffuseTexColor.rgb + vec3(1.0));
// Exposure tone mapping
vec3 mappedColor = vec3(1.0) - exp(-diffuseTexColor.rgb * exposure);
fragColor = vec4(mappedColor, 1);
}

BIN
res/textures/brickwall-normal.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
res/textures/brickwall.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

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
rsrc_windows_386.syso Executable file

Binary file not shown.

BIN
rsrc_windows_amd64.syso Executable file

Binary file not shown.

View File

@ -6,32 +6,48 @@ import (
)
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 (s *ShaderProgram) Bind() {
gl.UseProgram(s.Id)
}
func (s *ShaderProgram) UnBind() {
gl.UseProgram(0)
}

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

@ -3,6 +3,7 @@ package shaders
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
@ -11,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) {
@ -26,7 +28,7 @@ func NewShaderProgram() (ShaderProgram, error) {
return ShaderProgram{}, errors.New("failed to create shader program")
}
return ShaderProgram{ID: id}, nil
return ShaderProgram{Id: id}, nil
}
func LoadAndCompileCombinedShader(shaderPath string) (ShaderProgram, error) {
@ -43,8 +45,8 @@ func LoadAndCompileCombinedShader(shaderPath string) (ShaderProgram, error) {
func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
shaderSources := bytes.Split(shaderSrc, []byte("//shader:"))
if len(shaderSources) == 1 {
return ShaderProgram{}, errors.New("failed to read combined shader. Did not find '//shader:vertex' or '//shader:fragment'")
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()
@ -65,12 +67,15 @@ func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
var shdrType ShaderType
if bytes.HasPrefix(src, []byte("vertex")) {
src = src[6:]
shdrType = VertexShaderType
shdrType = ShaderType_Vertex
} else if bytes.HasPrefix(src, []byte("fragment")) {
src = src[8:]
shdrType = FragmentShaderType
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'")
return ShaderProgram{}, errors.New("unknown shader type. Must be '//shader:vertex' or '//shader:fragment' or '//shader:geometry'")
}
shdr, err := CompileShaderOfType(src, shdrType)
@ -83,7 +88,15 @@ func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
}
if loadedShdrCount == 0 {
return ShaderProgram{}, errors.New("no valid shaders found. Please put '//shader:vertex' or '//shader:fragment' before your shaders")
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()
@ -92,41 +105,40 @@ func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
func CompileShaderOfType(shaderSource []byte, shaderType ShaderType) (Shader, error) {
shaderID := gl.CreateShader(uint32(shaderType))
if shaderID == 0 {
logging.ErrLog.Println("Failed to create shader.")
return Shader{}, errors.New("failed to create shader")
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,30 +1,30 @@
package nmageimgui
import (
imgui "github.com/AllenDang/cimgui-go"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/materials"
"github.com/bloeys/nmage/timing"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/inkyblackness/imgui-go/v4"
"github.com/veandco/go-sdl2/sdl"
)
type ImguiInfo struct {
ImCtx *imgui.Context
ImCtx imgui.Context
Mat *materials.Material
Mat materials.Material
VaoID uint32
VboID uint32
IndexBufID uint32
TexID 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())
}
// 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)})
@ -35,9 +35,9 @@ func (i *ImguiInfo) FrameStart(winWidth, winHeight float32) {
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())
}
// if err := i.ImCtx.SetCurrent(); err != nil {
// assert.T(false, "Setting imgui ctx as current failed. Err: "+err.Error())
// }
imgui.Render()
@ -46,7 +46,7 @@ func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32)
return
}
drawData := imgui.RenderedDrawData()
drawData := imgui.CurrentDrawData()
drawData.ScaleClipRects(imgui.Vec2{
X: float32(fbWidth) / float32(winWidth),
Y: float32(fbHeight) / float32(winHeight),
@ -68,7 +68,7 @@ func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32)
i.Mat.Bind()
i.Mat.SetUnifInt32("Texture", 0)
//PERF: only update the ortho matrix on window resize
// @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.
@ -96,11 +96,11 @@ func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32)
// Draw
for _, list := range drawData.CommandLists() {
vertexBuffer, vertexBufferSize := list.VertexBuffer()
vertexBuffer, vertexBufferSize := list.GetVertexBuffer()
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW)
indexBuffer, indexBufferSize := list.IndexBuffer()
indexBuffer, indexBufferSize := list.GetIndexBuffer()
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, i.IndexBufID)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW)
@ -109,11 +109,12 @@ func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32)
cmd.CallUserCallback(list)
} else {
gl.BindTexture(gl.TEXTURE_2D, i.TexID)
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.DrawElementsBaseVertex(gl.TRIANGLES, int32(cmd.ElementCount()), uint32(drawType), gl.PtrOffset(cmd.IndexOffset()*indexSize), int32(cmd.VertexOffset()))
gl.DrawElementsBaseVertexWithOffset(gl.TRIANGLES, int32(cmd.ElemCount()), uint32(drawType), uintptr(int(cmd.IdxOffset())*indexSize), int32(cmd.VtxOffset()))
}
}
}
@ -124,14 +125,14 @@ func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32)
gl.Enable(gl.DEPTH_TEST)
}
func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *imgui.FontConfig, glyphRanges *imgui.GlyphRanges) imgui.Font {
func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *imgui.FontConfig, glyphRanges *imgui.GlyphRange) imgui.Font {
fontConfigToUse := imgui.DefaultFontConfig
fontConfigToUse := imgui.NewFontConfig()
if fontConfig != nil {
fontConfigToUse = *fontConfig
}
glyphRangesToUse := imgui.EmptyGlyphRanges
glyphRangesToUse := imgui.NewGlyphRange()
if glyphRanges != nil {
glyphRangesToUse = *glyphRanges
}
@ -139,16 +140,17 @@ func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *im
imIO := imgui.CurrentIO()
a := imIO.Fonts()
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse)
image := a.TextureDataAlpha8()
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse.Data())
pixels, width, height, _ := a.GetTextureDataAsAlpha8()
gl.BindTexture(gl.TEXTURE_2D, i.TexID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels)
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 imguiShdrSrc = `
const DefaultImguiShader = `
//shader:vertex
#version 410
@ -161,10 +163,24 @@ 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 = Color;
Frag_Color = srgba_to_linear(Color);
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
}
@ -184,32 +200,44 @@ void main()
}
`
func NewImGUI() ImguiInfo {
// NewImGui setups imgui using the passed shader.
// If the path is empty a default nMage shader is used
func NewImGui(shaderPath string) ImguiInfo {
imguiInfo := ImguiInfo{
ImCtx: imgui.CreateContext(nil),
Mat: materials.NewMaterialSrc("ImGUI Mat", []byte(imguiShdrSrc)),
var imguiMat materials.Material
if shaderPath == "" {
imguiMat = materials.NewMaterialSrc("ImGUI Mat", []byte(DefaultImguiShader))
} else {
imguiMat = materials.NewMaterial("ImGUI Mat", shaderPath)
}
imIO := imgui.CurrentIO()
imIO.SetBackendFlags(imIO.GetBackendFlags() | imgui.BackendFlagsRendererHasVtxOffset)
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)
gl.GenTextures(1, imguiInfo.TexID)
// Upload font to gpu
gl.BindTexture(gl.TEXTURE_2D, imguiInfo.TexID)
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)
image := imIO.Fonts().TextureDataAlpha8()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(image.Width), int32(image.Height), 0, gl.RED, gl.UNSIGNED_BYTE, image.Pixels)
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
imIO.Fonts().SetTextureID(imgui.TextureID(imguiInfo.TexID))
io.Fonts().SetTexID(imgui.TextureID(imguiInfo.TexID))
//Shader attributes
imguiInfo.Mat.Bind()
@ -218,35 +246,223 @@ func NewImGUI() ImguiInfo {
imguiInfo.Mat.EnableAttribute("Color")
imguiInfo.Mat.UnBind()
//Init imgui input mapping
keys := map[int]int{
imgui.KeyTab: sdl.SCANCODE_TAB,
imgui.KeyLeftArrow: sdl.SCANCODE_LEFT,
imgui.KeyRightArrow: sdl.SCANCODE_RIGHT,
imgui.KeyUpArrow: sdl.SCANCODE_UP,
imgui.KeyDownArrow: sdl.SCANCODE_DOWN,
imgui.KeyPageUp: sdl.SCANCODE_PAGEUP,
imgui.KeyPageDown: sdl.SCANCODE_PAGEDOWN,
imgui.KeyHome: sdl.SCANCODE_HOME,
imgui.KeyEnd: sdl.SCANCODE_END,
imgui.KeyInsert: sdl.SCANCODE_INSERT,
imgui.KeyDelete: sdl.SCANCODE_DELETE,
imgui.KeyBackspace: sdl.SCANCODE_BACKSPACE,
imgui.KeySpace: sdl.SCANCODE_BACKSPACE,
imgui.KeyEnter: sdl.SCANCODE_RETURN,
imgui.KeyEscape: sdl.SCANCODE_ESCAPE,
imgui.KeyA: sdl.SCANCODE_A,
imgui.KeyC: sdl.SCANCODE_C,
imgui.KeyV: sdl.SCANCODE_V,
imgui.KeyX: sdl.SCANCODE_X,
imgui.KeyY: sdl.SCANCODE_Y,
imgui.KeyZ: sdl.SCANCODE_Z,
}
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
for imguiKey, nativeKey := range keys {
imIO.KeyMap(imguiKey, nativeKey)
}
return imguiInfo
}
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
}
}