Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3574318552 | |||
| 05ccf3e158 | |||
| 4f5fd50660 | |||
| aaea27b543 | |||
| 039d09f888 | |||
| 1b83d7f9a7 | |||
| 201d9546b2 | |||
| c1d5033eb0 | |||
| 6f646540f9 | |||
| a99dd304ed | |||
| 4e45995ed0 | |||
| a735e01a77 | |||
| abb45e4c4a | |||
| 78ea3ae747 | |||
| b44b00d7e2 | |||
| 70dccd757e | |||
| d7cd5bfc8d | |||
| 3b8e5c06de | |||
| 8e9dbee002 | |||
| 7b2fb19618 | |||
| 9651f77348 | |||
| a16654107b | |||
| 855cbfaba3 | |||
| b025afe1b4 | |||
| 84cd8c28c8 | |||
| 7d5e3e2d82 | |||
| 23a6689346 | |||
| 36488ead04 | |||
| de77d5464e | |||
| 305982deca | |||
| 653a315631 | |||
| c971324b5a | |||
| b5a2479c16 | |||
| 6f54aecb5f | |||
| 7a25aea6ba | |||
| 3071b52c85 | |||
| 1b858bd4ac | |||
| d550767cb6 | |||
| 271b1c0cea | |||
| 0da031aa57 | |||
| 62194c4cad | |||
| bd79f6e274 | |||
| ac0ca8ee39 | |||
| 35ff496a9a | |||
| 52b77e017e | |||
| b85056dd31 | |||
| e5ea6f986f | |||
| c4853792a5 | |||
| 8cf9be2830 | |||
| 71acc2e9ab | |||
| 2690014fc5 | |||
| fe2aef6b6d | |||
| d4fe6d4071 | |||
| 51057b8a0d | |||
| e1bf0697fc | |||
| 901d8e2b5e | |||
| 89d04c9d24 | |||
| f1b6f3a7c0 | |||
| d1f47316ae | |||
| 709dc062cc | |||
| 660c41bc06 | |||
| 99f5548ce2 | |||
| 5a54b1b465 | |||
| 36ac96d641 | |||
| 577e6250a8 | |||
| c311a0981c | |||
| 064a932037 | |||
| 841a6e989c | |||
| 94942e55a1 | |||
| 15087ac542 | |||
| f16407629a | |||
| 592208d5c9 | |||
| fd74d58ad3 | |||
| 4c2fca48b3 | |||
| 50c2ab650f | |||
| 8e96cf7050 | |||
| 56e10049e9 | |||
| 2bfba880a9 | |||
| ffc9b6aa7c | |||
| f49c6bc9bb | |||
| c989505aa7 | |||
| 9ff1149191 | |||
| 42d99b3cc7 | |||
| 29832b9708 | |||
| 9a621d0669 | |||
| e893880f3b | |||
| cbe3d5111f | |||
| 3aa53852f3 | |||
| 46483352c7 | |||
| e38cd90a84 | |||
| 1109caef43 | |||
| e1e617e4e4 | |||
| 6dee7b0f1d |
28
.github/workflows/build-nmage.yml
vendored
Executable file
@ -0,0 +1,28 @@
|
||||
name: build-nmage
|
||||
|
||||
on:
|
||||
create:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-nmage-macos:
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
|
||||
- name: Install golang 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.18'
|
||||
|
||||
- name: Install assimp-go dylib
|
||||
run: sudo mkdir -p /usr/local/lib && sudo wget https://github.com/bloeys/assimp-go/releases/download/v0.4.2/libassimp_darwin_amd64.dylib -O /usr/local/lib/libassimp.5.dylib
|
||||
|
||||
- name: Install SDL2
|
||||
run: brew install sdl2{,_image,_mixer,_ttf,_gfx} pkg-config
|
||||
|
||||
- name: Clone nmage
|
||||
run: git clone https://github.com/bloeys/nmage
|
||||
|
||||
- name: build nmage
|
||||
working-directory: nmage
|
||||
run: go build .
|
||||
3
.gitignore
vendored
@ -14,4 +14,5 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
.vscode/
|
||||
imgui.ini
|
||||
imgui.ini
|
||||
*~
|
||||
@ -1,5 +1,7 @@
|
||||
# nMage
|
||||
|
||||
[](https://github.com/bloeys/nmage/actions/workflows/build-nmage.yml)
|
||||
|
||||
nMage is a (hopefully!) high performance 3D Game Engine written in Go being developed [live](https://twitch.tv/bloeys), with recordings posted on [YouTube](https://www.youtube.com/channel/UCCf4qyNGPVwpj1HYFGahs_A).
|
||||
|
||||
This project is being built with the goals being (in no particular order):
|
||||
@ -17,6 +19,7 @@ To run the project you need:
|
||||
* A C/C++ compiler installed and in your path
|
||||
* Windows: [MingW](https://www.mingw-w64.org/downloads/#mingw-builds) or similar
|
||||
* Mac/Linux: Should be installed by default, but if not try [GCC](https://gcc.gnu.org/) or [Clang](https://releases.llvm.org/download.html)
|
||||
* Install SDL2 by following their [requirements](https://github.com/veandco/go-sdl2#requirements).
|
||||
* Get the required [assimp-go](https://github.com/bloeys/assimp-go) DLLs/DyLibs and place them correctly by following the assimp-go [README](https://github.com/bloeys/assimp-go#using-assimp-go).
|
||||
|
||||
Then you can start nMage with `go run .`
|
||||
|
||||
13
assert/assert.go
Executable file
@ -0,0 +1,13 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/consts"
|
||||
"github.com/bloeys/nmage/logging"
|
||||
)
|
||||
|
||||
func T(check bool, msg string, args ...any) {
|
||||
|
||||
if consts.Debug && !check {
|
||||
logging.ErrLog.Panicf("Assert failed: "+msg, args...)
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package asserts
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/consts"
|
||||
"github.com/bloeys/nmage/logging"
|
||||
)
|
||||
|
||||
func True(check bool, msg string) {
|
||||
if consts.Debug && !check {
|
||||
logging.ErrLog.Panicln(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func False(check bool, msg string) {
|
||||
if consts.Debug && check {
|
||||
logging.ErrLog.Panicln(msg)
|
||||
}
|
||||
}
|
||||
32
assets/assets.go
Executable file
@ -0,0 +1,32 @@
|
||||
package assets
|
||||
|
||||
var (
|
||||
Textures = make(map[uint32]Texture)
|
||||
TexturePaths = make(map[string]uint32)
|
||||
)
|
||||
|
||||
func AddTextureToCache(t Texture) {
|
||||
|
||||
if t.Path != "" {
|
||||
if _, ok := TexturePaths[t.Path]; ok {
|
||||
return
|
||||
}
|
||||
println("Loaded texture from path:", t.Path)
|
||||
Textures[t.TexID] = t
|
||||
TexturePaths[t.Path] = t.TexID
|
||||
return
|
||||
}
|
||||
|
||||
println("Loaded in-mem texture with ID:", t.TexID)
|
||||
TexturePaths[t.Path] = t.TexID
|
||||
}
|
||||
|
||||
func GetTextureFromCacheID(texID uint32) (Texture, bool) {
|
||||
tex, ok := Textures[texID]
|
||||
return tex, ok
|
||||
}
|
||||
|
||||
func GetTextureFromCachePath(path string) (Texture, bool) {
|
||||
tex, ok := Textures[TexturePaths[path]]
|
||||
return tex, ok
|
||||
}
|
||||
308
assets/textures.go
Executable file
@ -0,0 +1,308 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type ColorFormat int
|
||||
|
||||
const (
|
||||
ColorFormat_RGBA8 ColorFormat = iota
|
||||
)
|
||||
|
||||
type Texture struct {
|
||||
//Path only exists for textures loaded from disk
|
||||
Path string
|
||||
TexID uint32
|
||||
Width int32
|
||||
Height int32
|
||||
Pixels []byte
|
||||
}
|
||||
|
||||
type TextureLoadOptions struct {
|
||||
TryLoadFromCache bool
|
||||
WriteToCache bool
|
||||
GenMipMaps bool
|
||||
KeepPixelsInMem bool
|
||||
}
|
||||
|
||||
type Cubemap struct {
|
||||
// These only exists for textures loaded from disk
|
||||
RightPath string
|
||||
LeftPath string
|
||||
TopPath string
|
||||
BotPath string
|
||||
FrontPath string
|
||||
BackPath string
|
||||
TexID uint32
|
||||
}
|
||||
|
||||
func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, error) {
|
||||
|
||||
if loadOptions == nil {
|
||||
loadOptions = &TextureLoadOptions{}
|
||||
}
|
||||
|
||||
if loadOptions.TryLoadFromCache {
|
||||
if tex, ok := GetTextureFromCachePath(file); ok {
|
||||
return tex, nil
|
||||
}
|
||||
}
|
||||
|
||||
//Load from disk
|
||||
fileBytes, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return Texture{}, err
|
||||
}
|
||||
|
||||
img, err := png.Decode(bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
return Texture{}, err
|
||||
}
|
||||
|
||||
tex := Texture{
|
||||
Path: file,
|
||||
}
|
||||
|
||||
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
|
||||
|
||||
//Prepare opengl stuff
|
||||
gl.GenTextures(1, &tex.TexID)
|
||||
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
|
||||
|
||||
// set the texture wrapping/filtering options (on the currently bound texture object)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
|
||||
// load and generate the texture
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
|
||||
|
||||
if loadOptions.GenMipMaps {
|
||||
gl.GenerateMipmap(tex.TexID)
|
||||
}
|
||||
|
||||
if loadOptions.WriteToCache {
|
||||
AddTextureToCache(tex)
|
||||
}
|
||||
|
||||
if !loadOptions.KeepPixelsInMem {
|
||||
tex.Pixels = nil
|
||||
}
|
||||
|
||||
return tex, nil
|
||||
}
|
||||
|
||||
func LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
|
||||
|
||||
if loadOptions == nil {
|
||||
loadOptions = &TextureLoadOptions{}
|
||||
}
|
||||
|
||||
tex := Texture{}
|
||||
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
|
||||
|
||||
//Prepare opengl stuff
|
||||
gl.GenTextures(1, &tex.TexID)
|
||||
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
|
||||
|
||||
// set the texture wrapping/filtering options (on the currently bound texture object)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
|
||||
// load and generate the texture
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
|
||||
|
||||
if loadOptions.GenMipMaps {
|
||||
gl.GenerateMipmap(tex.TexID)
|
||||
}
|
||||
|
||||
if loadOptions.WriteToCache {
|
||||
AddTextureToCache(tex)
|
||||
}
|
||||
|
||||
if !loadOptions.KeepPixelsInMem {
|
||||
tex.Pixels = nil
|
||||
}
|
||||
|
||||
return tex, nil
|
||||
}
|
||||
|
||||
func LoadTextureJpeg(file string, loadOptions *TextureLoadOptions) (Texture, error) {
|
||||
|
||||
if loadOptions == nil {
|
||||
loadOptions = &TextureLoadOptions{}
|
||||
}
|
||||
|
||||
if loadOptions.TryLoadFromCache {
|
||||
if tex, ok := GetTextureFromCachePath(file); ok {
|
||||
return tex, nil
|
||||
}
|
||||
}
|
||||
|
||||
//Load from disk
|
||||
fileBytes, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return Texture{}, err
|
||||
}
|
||||
|
||||
img, err := jpeg.Decode(bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
return Texture{}, err
|
||||
}
|
||||
|
||||
tex := Texture{
|
||||
Path: file,
|
||||
}
|
||||
|
||||
tex.Pixels, tex.Width, tex.Height = pixelsFromNrgbaPng(img)
|
||||
|
||||
//Prepare opengl stuff
|
||||
gl.GenTextures(1, &tex.TexID)
|
||||
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
|
||||
|
||||
// set the texture wrapping/filtering options (on the currently bound texture object)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
|
||||
// load and generate the texture
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
|
||||
|
||||
if loadOptions.GenMipMaps {
|
||||
gl.GenerateMipmap(tex.TexID)
|
||||
}
|
||||
|
||||
if loadOptions.WriteToCache {
|
||||
AddTextureToCache(tex)
|
||||
}
|
||||
|
||||
if !loadOptions.KeepPixelsInMem {
|
||||
tex.Pixels = nil
|
||||
}
|
||||
|
||||
return tex, nil
|
||||
}
|
||||
|
||||
func pixelsFromNrgbaPng(img image.Image) (pixels []byte, width, height int32) {
|
||||
|
||||
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
|
||||
//NOTE: We only support 8-bit channels (32-bit colors) for now
|
||||
i := 0
|
||||
width, height = int32(img.Bounds().Dx()), int32(img.Bounds().Dy())
|
||||
pixels = make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4)
|
||||
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
|
||||
for x := 0; x < img.Bounds().Dx(); x++ {
|
||||
|
||||
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
|
||||
pixels[i] = c.R
|
||||
pixels[i+1] = c.G
|
||||
pixels[i+2] = c.B
|
||||
pixels[i+3] = c.A
|
||||
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
|
||||
return pixels, width, height
|
||||
}
|
||||
|
||||
func pixelsFromNrgbaJpg(img image.Image) (pixels []byte, width, height int32) {
|
||||
|
||||
//NOTE: Load bottom left to top right because this is the texture coordinate system used by OpenGL
|
||||
//NOTE: We only support 8-bit channels (32-bit colors) for now
|
||||
i := 0
|
||||
width, height = int32(img.Bounds().Dx()), int32(img.Bounds().Dy())
|
||||
pixels = make([]byte, img.Bounds().Dx()*img.Bounds().Dy()*4)
|
||||
for y := img.Bounds().Dy() - 1; y >= 0; y-- {
|
||||
for x := 0; x < img.Bounds().Dx(); x++ {
|
||||
|
||||
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
|
||||
pixels[i] = c.R
|
||||
pixels[i+1] = c.G
|
||||
pixels[i+2] = c.B
|
||||
pixels[i+3] = c.A
|
||||
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
|
||||
return pixels, width, height
|
||||
}
|
||||
|
||||
func LoadCubemapTextures(rightTex, leftTex, topTex, botTex, frontTex, backTex string) (Cubemap, error) {
|
||||
|
||||
var imgDecoder func(r io.Reader) (image.Image, error)
|
||||
var pixelDecoder func(image.Image) ([]byte, int32, int32)
|
||||
ext := strings.ToLower(path.Ext(rightTex))
|
||||
if ext == ".jpg" || ext == ".jpeg" {
|
||||
imgDecoder = jpeg.Decode
|
||||
pixelDecoder = pixelsFromNrgbaJpg
|
||||
} else if ext == ".png" {
|
||||
imgDecoder = png.Decode
|
||||
pixelDecoder = pixelsFromNrgbaPng
|
||||
} else {
|
||||
return Cubemap{}, fmt.Errorf("unknown image extension: %s. Expected one of: .jpg, .jpeg, .png", ext)
|
||||
}
|
||||
|
||||
cmap := Cubemap{
|
||||
RightPath: rightTex,
|
||||
LeftPath: leftTex,
|
||||
TopPath: topTex,
|
||||
BotPath: botTex,
|
||||
FrontPath: frontTex,
|
||||
BackPath: backTex,
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &cmap.TexID)
|
||||
gl.BindTexture(gl.TEXTURE_CUBE_MAP, cmap.TexID)
|
||||
|
||||
// The order here matters
|
||||
texturePaths := []string{rightTex, leftTex, topTex, botTex, frontTex, backTex}
|
||||
for i := uint32(0); i < uint32(len(texturePaths)); i++ {
|
||||
|
||||
fPath := texturePaths[i]
|
||||
|
||||
//Load from disk
|
||||
fileBytes, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
return Cubemap{}, err
|
||||
}
|
||||
|
||||
img, err := imgDecoder(bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
return Cubemap{}, err
|
||||
}
|
||||
|
||||
pixels, width, height := pixelDecoder(img)
|
||||
|
||||
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X)+i, 0, gl.RGBA8, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&pixels[0]))
|
||||
}
|
||||
|
||||
// set the texture wrapping/filtering options (on the currently bound texture object)
|
||||
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
|
||||
|
||||
return cmap, nil
|
||||
}
|
||||
33
buffers/buf_usage.go
Executable file
@ -0,0 +1,33 @@
|
||||
package buffers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
type BufUsage int
|
||||
|
||||
const (
|
||||
//Buffer is set only once and used many times
|
||||
BufUsage_Static BufUsage = iota
|
||||
//Buffer is changed a lot and used many times
|
||||
BufUsage_Dynamic
|
||||
//Buffer is set only once and used by the GPU at most a few times
|
||||
BufUsage_Stream
|
||||
)
|
||||
|
||||
func (b BufUsage) ToGL() uint32 {
|
||||
switch b {
|
||||
case BufUsage_Static:
|
||||
return gl.STATIC_DRAW
|
||||
case BufUsage_Dynamic:
|
||||
return gl.DYNAMIC_DRAW
|
||||
case BufUsage_Stream:
|
||||
return gl.STREAM_DRAW
|
||||
}
|
||||
|
||||
assert.T(false, fmt.Sprintf("Unexpected BufUsage value '%v'", b))
|
||||
return 0
|
||||
}
|
||||
@ -5,169 +5,130 @@ import (
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
type BufGLType int
|
||||
|
||||
const (
|
||||
BufGLTypeUnknown BufGLType = 0
|
||||
//Generic array of data. Should be used for most data like vertex positions, vertex colors etc.
|
||||
BufGLTypeArray BufGLType = gl.ARRAY_BUFFER
|
||||
BufGLTypeIndices BufGLType = gl.ELEMENT_ARRAY_BUFFER
|
||||
)
|
||||
|
||||
type BufType int
|
||||
|
||||
const (
|
||||
BufTypeUnknown BufType = iota
|
||||
BufTypeVertPos
|
||||
BufTypeColor
|
||||
BufTypeIndex
|
||||
BufTypeNormal
|
||||
)
|
||||
|
||||
func (bt BufType) GetBufferGLType() BufGLType {
|
||||
switch bt {
|
||||
|
||||
case BufTypeNormal:
|
||||
fallthrough
|
||||
case BufTypeColor:
|
||||
fallthrough
|
||||
case BufTypeVertPos:
|
||||
return BufGLTypeArray
|
||||
|
||||
case BufTypeIndex:
|
||||
return BufGLTypeIndices
|
||||
default:
|
||||
logging.WarnLog.Println("Unknown BufferType. BufferType: ", bt)
|
||||
return BufGLTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
type BufUsage int
|
||||
|
||||
const (
|
||||
//Buffer is set only once and used many times
|
||||
BufUsageStatic BufUsage = gl.STATIC_DRAW
|
||||
//Buffer is changed a lot and used many times
|
||||
BufUsageDynamic BufUsage = gl.DYNAMIC_DRAW
|
||||
//Buffer is set only once and used by the GPU at most a few times
|
||||
BufUsageStream BufUsage = gl.STREAM_DRAW
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
ID uint32
|
||||
Type BufType
|
||||
GLType BufGLType
|
||||
DataTypeInfo
|
||||
VAOID uint32
|
||||
// BufID is the ID of the VBO
|
||||
BufID uint32
|
||||
// IndexBufID is the ID of the index/element buffer
|
||||
IndexBufID uint32
|
||||
// IndexBufCount is the number of elements in the index buffer
|
||||
// Updated on SetIndexBufData
|
||||
IndexBufCount int32
|
||||
// IndexBufCount int32
|
||||
Stride int32
|
||||
|
||||
//DataLen is the number of elements in the uploaded to the buffer
|
||||
DataLen int32
|
||||
layout []Element
|
||||
}
|
||||
|
||||
func (b *Buffer) Activate() {
|
||||
gl.BindBuffer(uint32(b.GLType), b.ID)
|
||||
func (b *Buffer) Bind() {
|
||||
gl.BindVertexArray(b.VAOID)
|
||||
}
|
||||
|
||||
func (b *Buffer) Deactivate() {
|
||||
gl.BindBuffer(uint32(b.GLType), 0)
|
||||
}
|
||||
|
||||
type BufferObject struct {
|
||||
VAOID uint32
|
||||
VertPosBuf *Buffer
|
||||
NormalBuf *Buffer
|
||||
ColorBuf *Buffer
|
||||
IndexBuf *Buffer
|
||||
}
|
||||
|
||||
func (bo *BufferObject) GenBuffer(data []float32, bufUsage BufUsage, bufType BufType, bufDataType DataType) {
|
||||
|
||||
gl.BindVertexArray(bo.VAOID)
|
||||
|
||||
//Create vertex buffer object
|
||||
var vboID uint32
|
||||
gl.GenBuffers(1, &vboID)
|
||||
if vboID == 0 {
|
||||
logging.ErrLog.Println("Failed to create openGL buffer")
|
||||
}
|
||||
|
||||
buf := &Buffer{
|
||||
ID: vboID,
|
||||
Type: bufType,
|
||||
GLType: bufType.GetBufferGLType(),
|
||||
DataTypeInfo: GetDataTypeInfo(bufDataType),
|
||||
DataLen: int32(len(data)),
|
||||
}
|
||||
bo.SetBuffer(buf)
|
||||
|
||||
//Fill buffer with data
|
||||
gl.BindBuffer(uint32(buf.GLType), buf.ID)
|
||||
gl.BufferData(uint32(buf.GLType), int(buf.DataTypeInfo.ElementSize)*len(data), gl.Ptr(data), uint32(bufUsage))
|
||||
|
||||
//Unbind everything
|
||||
gl.BindVertexArray(0)
|
||||
gl.BindBuffer(uint32(buf.GLType), 0)
|
||||
}
|
||||
|
||||
func (bo *BufferObject) GenBufferUint32(data []uint32, bufUsage BufUsage, bufType BufType, bufDataType DataType) {
|
||||
|
||||
gl.BindVertexArray(bo.VAOID)
|
||||
|
||||
//Create vertex buffer object
|
||||
var vboID uint32
|
||||
gl.GenBuffers(1, &vboID)
|
||||
if vboID == 0 {
|
||||
logging.ErrLog.Println("Failed to create openGL buffer")
|
||||
}
|
||||
|
||||
buf := &Buffer{
|
||||
ID: vboID,
|
||||
Type: bufType,
|
||||
GLType: bufType.GetBufferGLType(),
|
||||
DataTypeInfo: GetDataTypeInfo(bufDataType),
|
||||
DataLen: int32(len(data)),
|
||||
}
|
||||
bo.SetBuffer(buf)
|
||||
|
||||
//Fill buffer with data
|
||||
gl.BindBuffer(uint32(buf.GLType), buf.ID)
|
||||
gl.BufferData(uint32(buf.GLType), int(buf.DataTypeInfo.ElementSize)*len(data), gl.Ptr(data), uint32(bufUsage))
|
||||
|
||||
//Unbind everything
|
||||
gl.BindVertexArray(0)
|
||||
gl.BindBuffer(uint32(buf.GLType), 0)
|
||||
}
|
||||
|
||||
func (bo *BufferObject) SetBuffer(buf *Buffer) {
|
||||
|
||||
switch buf.Type {
|
||||
case BufTypeVertPos:
|
||||
bo.VertPosBuf = buf
|
||||
case BufTypeNormal:
|
||||
bo.NormalBuf = buf
|
||||
case BufTypeColor:
|
||||
bo.ColorBuf = buf
|
||||
case BufTypeIndex:
|
||||
bo.IndexBuf = buf
|
||||
default:
|
||||
logging.WarnLog.Println("Unknown buffer type in SetBuffer. Type:", buf.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (bo *BufferObject) Activate() {
|
||||
gl.BindVertexArray(bo.VAOID)
|
||||
}
|
||||
|
||||
func (bo *BufferObject) Deactivate() {
|
||||
func (b *Buffer) UnBind() {
|
||||
gl.BindVertexArray(0)
|
||||
}
|
||||
|
||||
func NewBufferObject() *BufferObject {
|
||||
func (b *Buffer) SetData(values []float32) {
|
||||
|
||||
var vaoID uint32
|
||||
gl.GenVertexArrays(1, &vaoID)
|
||||
if vaoID == 0 {
|
||||
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.VertexAttribPointerWithOffset(uint32(i), layout[i].ElementType.CompCount(), layout[i].ElementType.GLType(), false, b.Stride, uintptr(layout[i].Offset))
|
||||
}
|
||||
|
||||
b.UnBind()
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
|
||||
func NewBuffer(layout ...Element) Buffer {
|
||||
|
||||
b := Buffer{}
|
||||
gl.GenVertexArrays(1, &b.VAOID)
|
||||
if b.VAOID == 0 {
|
||||
logging.ErrLog.Println("Failed to create openGL vertex array object")
|
||||
}
|
||||
|
||||
return &BufferObject{VAOID: vaoID}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
package buffers
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/logging"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
type DataType int
|
||||
|
||||
const (
|
||||
DataTypeUnknown DataType = iota
|
||||
DataTypeUint32
|
||||
DataTypeInt32
|
||||
DataTypeFloat32
|
||||
DataTypeFloat64
|
||||
|
||||
DataTypeVec2
|
||||
DataTypeVec3
|
||||
DataTypeVec4
|
||||
)
|
||||
|
||||
type DataTypeInfo struct {
|
||||
//ElementSize is size in bytes of one element (e.g. for vec3 its 4)
|
||||
ElementSize int32
|
||||
//ElementCount is number of elements (e.g. for vec3 its 3)
|
||||
ElementCount int32
|
||||
//ElementType is the type of each primitive (e.g. for vec3 its gl.FLOAT)
|
||||
ElementType uint32
|
||||
//GLType is the type of the variable represented (e.g. for vec3 its gl.FLOAT_VEC2)
|
||||
GLType uint32
|
||||
}
|
||||
|
||||
//GetSize returns the total size in bytes (e.g. for vec3 its 4*3)
|
||||
func (dti *DataTypeInfo) GetSize() int32 {
|
||||
return dti.ElementSize * dti.ElementCount
|
||||
}
|
||||
|
||||
func GetDataTypeInfo(dt DataType) DataTypeInfo {
|
||||
|
||||
switch dt {
|
||||
case DataTypeUint32:
|
||||
fallthrough
|
||||
case DataTypeInt32:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 4,
|
||||
ElementCount: 1,
|
||||
ElementType: gl.INT,
|
||||
GLType: gl.INT,
|
||||
}
|
||||
|
||||
case DataTypeFloat32:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 4,
|
||||
ElementCount: 1,
|
||||
ElementType: gl.FLOAT,
|
||||
GLType: gl.FLOAT,
|
||||
}
|
||||
case DataTypeFloat64:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 8,
|
||||
ElementCount: 1,
|
||||
ElementType: gl.DOUBLE,
|
||||
GLType: gl.DOUBLE,
|
||||
}
|
||||
|
||||
case DataTypeVec2:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 4,
|
||||
ElementCount: 2,
|
||||
ElementType: gl.FLOAT,
|
||||
GLType: gl.FLOAT_VEC2,
|
||||
}
|
||||
case DataTypeVec3:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 4,
|
||||
ElementCount: 3,
|
||||
ElementType: gl.FLOAT,
|
||||
GLType: gl.FLOAT_VEC3,
|
||||
}
|
||||
case DataTypeVec4:
|
||||
return DataTypeInfo{
|
||||
ElementSize: 4,
|
||||
ElementCount: 4,
|
||||
ElementType: gl.FLOAT,
|
||||
GLType: gl.FLOAT_VEC4,
|
||||
}
|
||||
|
||||
default:
|
||||
logging.WarnLog.Println("Unknown data type passed. DataType:", dt)
|
||||
return DataTypeInfo{}
|
||||
}
|
||||
}
|
||||
122
buffers/element.go
Executable file
@ -0,0 +1,122 @@
|
||||
package buffers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
//Element represents an element that makes up a buffer (e.g. Vec3 at an offset of 12 bytes)
|
||||
type Element struct {
|
||||
Offset int
|
||||
ElementType
|
||||
}
|
||||
|
||||
//ElementType is the type of an element thats makes up a buffer (e.g. Vec3)
|
||||
type ElementType int
|
||||
|
||||
const (
|
||||
DataTypeUnknown ElementType = iota
|
||||
DataTypeUint32
|
||||
DataTypeInt32
|
||||
DataTypeFloat32
|
||||
|
||||
DataTypeVec2
|
||||
DataTypeVec3
|
||||
DataTypeVec4
|
||||
)
|
||||
|
||||
func (dt ElementType) GLType() uint32 {
|
||||
|
||||
switch dt {
|
||||
case DataTypeUint32:
|
||||
return gl.UNSIGNED_INT
|
||||
case DataTypeInt32:
|
||||
return gl.INT
|
||||
|
||||
case DataTypeFloat32:
|
||||
fallthrough
|
||||
case DataTypeVec2:
|
||||
fallthrough
|
||||
case DataTypeVec3:
|
||||
fallthrough
|
||||
case DataTypeVec4:
|
||||
return gl.FLOAT
|
||||
|
||||
default:
|
||||
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//CompSize returns the size in bytes for one component of the type (e.g. for Vec2 its 4)
|
||||
func (dt ElementType) CompSize() int32 {
|
||||
|
||||
switch dt {
|
||||
case DataTypeUint32:
|
||||
fallthrough
|
||||
case DataTypeFloat32:
|
||||
fallthrough
|
||||
case DataTypeInt32:
|
||||
fallthrough
|
||||
case DataTypeVec2:
|
||||
fallthrough
|
||||
case DataTypeVec3:
|
||||
fallthrough
|
||||
case DataTypeVec4:
|
||||
return 4
|
||||
|
||||
default:
|
||||
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//CompCount returns the number of components in the element (e.g. for Vec2 its 2)
|
||||
func (dt ElementType) CompCount() int32 {
|
||||
|
||||
switch dt {
|
||||
case DataTypeUint32:
|
||||
fallthrough
|
||||
case DataTypeFloat32:
|
||||
fallthrough
|
||||
case DataTypeInt32:
|
||||
return 1
|
||||
|
||||
case DataTypeVec2:
|
||||
return 2
|
||||
case DataTypeVec3:
|
||||
return 3
|
||||
case DataTypeVec4:
|
||||
return 4
|
||||
|
||||
default:
|
||||
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//Size returns the total size in bytes (e.g. for vec3 its 3*4=12 bytes)
|
||||
func (dt ElementType) Size() int32 {
|
||||
|
||||
switch dt {
|
||||
case DataTypeUint32:
|
||||
fallthrough
|
||||
case DataTypeFloat32:
|
||||
fallthrough
|
||||
case DataTypeInt32:
|
||||
return 4
|
||||
|
||||
case DataTypeVec2:
|
||||
return 2 * 4
|
||||
case DataTypeVec3:
|
||||
return 3 * 4
|
||||
case DataTypeVec4:
|
||||
return 4 * 4
|
||||
|
||||
default:
|
||||
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
100
camera/camera.go
Executable file
@ -0,0 +1,100 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
)
|
||||
|
||||
type Type int32
|
||||
|
||||
const (
|
||||
Type_Unknown Type = iota
|
||||
Type_Perspective
|
||||
Type_Orthographic
|
||||
)
|
||||
|
||||
type Camera struct {
|
||||
Type Type
|
||||
|
||||
Pos gglm.Vec3
|
||||
Forward gglm.Vec3
|
||||
WorldUp gglm.Vec3
|
||||
|
||||
NearClip float32
|
||||
FarClip float32
|
||||
|
||||
// Perspective data
|
||||
Fov float32
|
||||
AspectRatio float32
|
||||
|
||||
// Ortho data
|
||||
Left, Right, Top, Bottom float32
|
||||
|
||||
// Matrices
|
||||
ViewMat gglm.Mat4
|
||||
ProjMat gglm.Mat4
|
||||
}
|
||||
|
||||
// Update recalculates view matrix and projection matrix.
|
||||
// Should be called whenever a camera parameter changes
|
||||
func (c *Camera) Update() {
|
||||
|
||||
c.ViewMat = gglm.LookAtRH(&c.Pos, c.Pos.Clone().Add(&c.Forward), &c.WorldUp).Mat4
|
||||
|
||||
if c.Type == Type_Perspective {
|
||||
c.ProjMat = *gglm.Perspective(c.Fov, c.AspectRatio, c.NearClip, c.FarClip)
|
||||
} else {
|
||||
c.ProjMat = gglm.Ortho(c.Left, c.Right, c.Top, c.Bottom, c.NearClip, c.FarClip).Mat4
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRotation calculates a new forward vector and then calls camera.Update()
|
||||
func (c *Camera) UpdateRotation(pitch, yaw float32) {
|
||||
|
||||
dir := gglm.NewVec3(
|
||||
gglm.Cos32(yaw)*gglm.Cos32(pitch),
|
||||
gglm.Sin32(pitch),
|
||||
gglm.Sin32(yaw)*gglm.Cos32(pitch),
|
||||
)
|
||||
c.Forward = *dir.Normalize()
|
||||
c.Update()
|
||||
}
|
||||
|
||||
func NewPerspective(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, fovRadians, aspectRatio float32) *Camera {
|
||||
|
||||
cam := &Camera{
|
||||
Type: Type_Perspective,
|
||||
Pos: *pos,
|
||||
Forward: *forward,
|
||||
WorldUp: *worldUp,
|
||||
|
||||
NearClip: nearClip,
|
||||
FarClip: farClip,
|
||||
|
||||
Fov: fovRadians,
|
||||
AspectRatio: aspectRatio,
|
||||
}
|
||||
cam.Update()
|
||||
|
||||
return cam
|
||||
}
|
||||
|
||||
func NewOrthographic(pos, forward, worldUp *gglm.Vec3, nearClip, farClip, left, right, top, bottom float32) *Camera {
|
||||
|
||||
cam := &Camera{
|
||||
Type: Type_Orthographic,
|
||||
Pos: *pos,
|
||||
Forward: *forward,
|
||||
WorldUp: *worldUp,
|
||||
|
||||
NearClip: nearClip,
|
||||
FarClip: farClip,
|
||||
|
||||
Left: left,
|
||||
Right: right,
|
||||
Top: top,
|
||||
Bottom: bottom,
|
||||
}
|
||||
cam.Update()
|
||||
|
||||
return cam
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
//go:build debug
|
||||
//go:build !release
|
||||
|
||||
package consts
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//go:build !debug
|
||||
//go:build release
|
||||
|
||||
package consts
|
||||
|
||||
|
||||
215
engine/engine.go
Executable file
@ -0,0 +1,215 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
imgui "github.com/AllenDang/cimgui-go"
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/bloeys/nmage/input"
|
||||
"github.com/bloeys/nmage/renderer"
|
||||
"github.com/bloeys/nmage/timing"
|
||||
nmageimgui "github.com/bloeys/nmage/ui/imgui"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
var (
|
||||
isInited = false
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
// @TODO: Would be nice to have imgui package process its own events via a callback instead of it being part of engine code
|
||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||
|
||||
//Fire callbacks
|
||||
for i := 0; i < len(w.EventCallbacks); i++ {
|
||||
w.EventCallbacks[i](event)
|
||||
}
|
||||
|
||||
//Internal processing
|
||||
switch e := event.(type) {
|
||||
|
||||
case *sdl.MouseWheelEvent:
|
||||
|
||||
input.HandleMouseWheelEvent(e)
|
||||
|
||||
xDelta, yDelta := input.GetMouseWheelMotion()
|
||||
imIo.AddMouseWheelDelta(float32(xDelta), float32(yDelta))
|
||||
|
||||
case *sdl.KeyboardEvent:
|
||||
|
||||
input.HandleKeyboardEvent(e)
|
||||
imIo.AddKeyEvent(nmageimgui.SdlScancodeToImGuiKey(e.Keysym.Scancode), e.Type == sdl.KEYDOWN)
|
||||
|
||||
// Send modifier key updates to imgui
|
||||
if e.Keysym.Sym == sdl.K_LCTRL || e.Keysym.Sym == sdl.K_RCTRL {
|
||||
imIo.SetKeyCtrl(e.Type == sdl.KEYDOWN)
|
||||
}
|
||||
|
||||
if e.Keysym.Sym == sdl.K_LSHIFT || e.Keysym.Sym == sdl.K_RSHIFT {
|
||||
imIo.SetKeyShift(e.Type == sdl.KEYDOWN)
|
||||
}
|
||||
|
||||
if e.Keysym.Sym == sdl.K_LALT || e.Keysym.Sym == sdl.K_RALT {
|
||||
imIo.SetKeyAlt(e.Type == sdl.KEYDOWN)
|
||||
}
|
||||
|
||||
if e.Keysym.Sym == sdl.K_LGUI || e.Keysym.Sym == sdl.K_RGUI {
|
||||
imIo.SetKeySuper(e.Type == sdl.KEYDOWN)
|
||||
}
|
||||
|
||||
case *sdl.TextInputEvent:
|
||||
imIo.AddInputCharactersUTF8(e.GetText())
|
||||
|
||||
case *sdl.MouseButtonEvent:
|
||||
input.HandleMouseBtnEvent(e)
|
||||
|
||||
case *sdl.MouseMotionEvent:
|
||||
input.HandleMouseMotionEvent(e)
|
||||
|
||||
case *sdl.WindowEvent:
|
||||
if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED {
|
||||
w.handleWindowResize()
|
||||
}
|
||||
|
||||
case *sdl.QuitEvent:
|
||||
input.HandleQuitEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
|
||||
x, y, _ := sdl.GetMouseState()
|
||||
imIo.SetMousePos(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))
|
||||
}
|
||||
|
||||
func (w *Window) handleWindowResize() {
|
||||
|
||||
fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize()
|
||||
if fbWidth <= 0 || fbHeight <= 0 {
|
||||
return
|
||||
}
|
||||
gl.Viewport(0, 0, fbWidth, fbHeight)
|
||||
}
|
||||
|
||||
func (w *Window) Destroy() error {
|
||||
return w.SDLWin.Destroy()
|
||||
}
|
||||
|
||||
func Init() error {
|
||||
|
||||
isInited = true
|
||||
|
||||
runtime.LockOSThread()
|
||||
timing.Init()
|
||||
err := initSDL()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func initSDL() error {
|
||||
|
||||
err := sdl.Init(sdl.INIT_TIMER | sdl.INIT_VIDEO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sdl.ShowCursor(1)
|
||||
|
||||
sdl.GLSetAttribute(sdl.MAJOR_VERSION, 4)
|
||||
sdl.GLSetAttribute(sdl.MINOR_VERSION, 1)
|
||||
|
||||
// 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_DOUBLEBUFFER, 1)
|
||||
sdl.GLSetAttribute(sdl.GL_DEPTH_SIZE, 24)
|
||||
sdl.GLSetAttribute(sdl.GL_STENCIL_SIZE, 8)
|
||||
|
||||
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
|
||||
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags, rend)
|
||||
}
|
||||
|
||||
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
|
||||
return createWindow(title, -1, -1, width, height, WindowFlags_OPENGL|flags, rend)
|
||||
}
|
||||
|
||||
func createWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (*Window, error) {
|
||||
|
||||
assert.T(isInited, "engine.Init was not called!")
|
||||
if x == -1 && y == -1 {
|
||||
x = sdl.WINDOWPOS_CENTERED
|
||||
y = sdl.WINDOWPOS_CENTERED
|
||||
}
|
||||
|
||||
sdlWin, err := sdl.CreateWindow(title, x, y, width, height, uint32(flags))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
win := &Window{
|
||||
SDLWin: sdlWin,
|
||||
EventCallbacks: make([]func(sdl.Event), 0),
|
||||
Rend: rend,
|
||||
}
|
||||
|
||||
win.GlCtx, err = sdlWin.GLCreateContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = initOpenGL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return win, err
|
||||
}
|
||||
|
||||
func initOpenGL() error {
|
||||
|
||||
if err := gl.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gl.Enable(gl.DEPTH_TEST)
|
||||
gl.Enable(gl.CULL_FACE)
|
||||
gl.CullFace(gl.BACK)
|
||||
gl.FrontFace(gl.CCW)
|
||||
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetVSync(enabled bool) {
|
||||
assert.T(isInited, "engine.Init was not called!")
|
||||
|
||||
if enabled {
|
||||
sdl.GLSetSwapInterval(1)
|
||||
} else {
|
||||
sdl.GLSetSwapInterval(0)
|
||||
}
|
||||
}
|
||||
68
engine/game.go
Executable file
@ -0,0 +1,68 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/timing"
|
||||
nmageimgui "github.com/bloeys/nmage/ui/imgui"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
var (
|
||||
isRunning = false
|
||||
)
|
||||
|
||||
type Game interface {
|
||||
Init()
|
||||
|
||||
Update()
|
||||
Render()
|
||||
FrameEnd()
|
||||
|
||||
DeInit()
|
||||
}
|
||||
|
||||
func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
|
||||
|
||||
isRunning = true
|
||||
|
||||
// Run init with an active Imgui frame to allow init full imgui access
|
||||
timing.FrameStarted()
|
||||
w.handleInputs()
|
||||
|
||||
width, height := w.SDLWin.GetSize()
|
||||
ui.FrameStart(float32(width), float32(height))
|
||||
|
||||
g.Init()
|
||||
|
||||
fbWidth, fbHeight := w.SDLWin.GLGetDrawableSize()
|
||||
ui.Render(float32(width), float32(height), fbWidth, fbHeight)
|
||||
|
||||
timing.FrameEnded()
|
||||
|
||||
for isRunning {
|
||||
|
||||
//PERF: Cache these
|
||||
width, height = w.SDLWin.GetSize()
|
||||
fbWidth, fbHeight = w.SDLWin.GLGetDrawableSize()
|
||||
|
||||
timing.FrameStarted()
|
||||
w.handleInputs()
|
||||
ui.FrameStart(float32(width), float32(height))
|
||||
|
||||
g.Update()
|
||||
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
g.Render()
|
||||
ui.Render(float32(width), float32(height), fbWidth, fbHeight)
|
||||
w.SDLWin.GLSwap()
|
||||
|
||||
g.FrameEnd()
|
||||
w.Rend.FrameEnd()
|
||||
timing.FrameEnded()
|
||||
}
|
||||
|
||||
g.DeInit()
|
||||
}
|
||||
|
||||
func Quit() {
|
||||
isRunning = false
|
||||
}
|
||||
29
engine/windowflags.go
Executable file
@ -0,0 +1,29 @@
|
||||
package engine
|
||||
|
||||
import "github.com/veandco/go-sdl2/sdl"
|
||||
|
||||
type WindowFlags int
|
||||
|
||||
const (
|
||||
WindowFlags_FULLSCREEN WindowFlags = sdl.WINDOW_FULLSCREEN
|
||||
WindowFlags_OPENGL WindowFlags = sdl.WINDOW_OPENGL
|
||||
WindowFlags_SHOWN WindowFlags = sdl.WINDOW_SHOWN
|
||||
WindowFlags_HIDDEN WindowFlags = sdl.WINDOW_HIDDEN
|
||||
WindowFlags_BORDERLESS WindowFlags = sdl.WINDOW_BORDERLESS
|
||||
WindowFlags_RESIZABLE WindowFlags = sdl.WINDOW_RESIZABLE
|
||||
WindowFlags_MINIMIZED WindowFlags = sdl.WINDOW_MINIMIZED
|
||||
WindowFlags_MAXIMIZED WindowFlags = sdl.WINDOW_MAXIMIZED
|
||||
WindowFlags_INPUT_GRABBED WindowFlags = sdl.WINDOW_INPUT_GRABBED
|
||||
WindowFlags_INPUT_FOCUS WindowFlags = sdl.WINDOW_INPUT_FOCUS
|
||||
WindowFlags_MOUSE_FOCUS WindowFlags = sdl.WINDOW_MOUSE_FOCUS
|
||||
WindowFlags_FULLSCREEN_DESKTOP WindowFlags = sdl.WINDOW_FULLSCREEN_DESKTOP
|
||||
WindowFlags_FOREIGN WindowFlags = sdl.WINDOW_FOREIGN
|
||||
WindowFlags_ALLOW_HIGHDPI WindowFlags = sdl.WINDOW_ALLOW_HIGHDPI
|
||||
WindowFlags_MOUSE_CAPTURE WindowFlags = sdl.WINDOW_MOUSE_CAPTURE
|
||||
WindowFlags_ALWAYS_ON_TOP WindowFlags = sdl.WINDOW_ALWAYS_ON_TOP
|
||||
WindowFlags_SKIP_TASKBAR WindowFlags = sdl.WINDOW_SKIP_TASKBAR
|
||||
WindowFlags_UTILITY WindowFlags = sdl.WINDOW_UTILITY
|
||||
WindowFlags_TOOLTIP WindowFlags = sdl.WINDOW_TOOLTIP
|
||||
WindowFlags_POPUP_MENU WindowFlags = sdl.WINDOW_POPUP_MENU
|
||||
// WindowFlags_VULKAN WindowFlags = sdl.WINDOW_VULKAN
|
||||
)
|
||||
26
entity/base_comp.go
Executable file
@ -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
@ -0,0 +1,73 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/bloeys/nmage/registry"
|
||||
)
|
||||
|
||||
type Comp interface {
|
||||
// This ensures that implementors of the Comp interface
|
||||
// always embed BaseComp
|
||||
baseComp()
|
||||
|
||||
Name() string
|
||||
Init(parentHandle registry.Handle)
|
||||
Update()
|
||||
Destroy()
|
||||
}
|
||||
|
||||
func NewCompContainer() CompContainer {
|
||||
return CompContainer{Comps: []Comp{}}
|
||||
}
|
||||
|
||||
type CompContainer struct {
|
||||
Comps []Comp
|
||||
}
|
||||
|
||||
func AddComp[T Comp](entityHandle registry.Handle, cc *CompContainer, c T) {
|
||||
|
||||
assert.T(!HasComp[T](cc), "Entity with id '%v' already has component of type '%T'", entityHandle, c)
|
||||
|
||||
cc.Comps = append(cc.Comps, c)
|
||||
c.Init(entityHandle)
|
||||
}
|
||||
|
||||
func HasComp[T Comp](e *CompContainer) bool {
|
||||
|
||||
for i := 0; i < len(e.Comps); i++ {
|
||||
|
||||
_, ok := e.Comps[i].(T)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetComp[T Comp](e *CompContainer) (out T) {
|
||||
|
||||
for i := 0; i < len(e.Comps); i++ {
|
||||
|
||||
comp, ok := e.Comps[i].(T)
|
||||
if ok {
|
||||
return comp
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// DestroyComp calls Destroy on the component and then removes it from the entities component list
|
||||
func DestroyComp[T Comp](e *CompContainer) {
|
||||
|
||||
for i := 0; i < len(e.Comps); i++ {
|
||||
|
||||
comp, ok := e.Comps[i].(T)
|
||||
if ok {
|
||||
comp.Destroy()
|
||||
e.Comps = append(e.Comps[:i], e.Comps[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
7
entity/entity.go
Executable file
@ -0,0 +1,7 @@
|
||||
package entity
|
||||
|
||||
import "github.com/bloeys/nmage/registry"
|
||||
|
||||
type Entity interface {
|
||||
GetHandle() registry.Handle
|
||||
}
|
||||
13
go.mod
@ -1,13 +1,14 @@
|
||||
module github.com/bloeys/nmage
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require github.com/veandco/go-sdl2 v0.4.10
|
||||
require github.com/veandco/go-sdl2 v0.4.35
|
||||
|
||||
require github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784
|
||||
require github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6
|
||||
|
||||
require (
|
||||
github.com/bloeys/assimp-go v0.3.3
|
||||
github.com/bloeys/gglm v0.3.1
|
||||
github.com/inkyblackness/imgui-go/v4 v4.3.0
|
||||
github.com/bloeys/assimp-go v0.4.4
|
||||
github.com/bloeys/gglm v0.43.0
|
||||
)
|
||||
|
||||
require github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2
|
||||
|
||||
31
go.sum
@ -1,21 +1,10 @@
|
||||
github.com/bloeys/assimp-go v0.3.1 h1:GANPXH8ER/4B/XsxZOw03GLZi2qKiWapSJ9dntrGoic=
|
||||
github.com/bloeys/assimp-go v0.3.1/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
|
||||
github.com/bloeys/assimp-go v0.3.2 h1:CsKnLloWZyn6uYNNaQE2Jq2Q+yH4d71A+CxbpU2flng=
|
||||
github.com/bloeys/assimp-go v0.3.2/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
|
||||
github.com/bloeys/assimp-go v0.3.3 h1:36Cqdsv/vVWg7mx6Kvu++1Z0SftdAQF4a+ApllpVT4M=
|
||||
github.com/bloeys/assimp-go v0.3.3/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
|
||||
github.com/bloeys/gglm v0.3.1 h1:Sy9upW7SBsBfDXrSmEhid3aQ+7J7itej+upwcxOnPMQ=
|
||||
github.com/bloeys/gglm v0.3.1/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 h1:1Zi56D0LNfvkzM+BdoxKryvUEdyWO7LP8oRT+oSYJW0=
|
||||
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/inkyblackness/imgui-go/v4 v4.3.0 h1:iyAzqWXq/dG5+6ckDPhGivtrIo6AywGQMvENKzun04s=
|
||||
github.com/inkyblackness/imgui-go/v4 v4.3.0/go.mod h1:g8SAGtOYUP7rYaOB2AsVKCEHmPMDmJKgt4z6d+flhb0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/veandco/go-sdl2 v0.4.10 h1:8QoD2bhWl7SbQDflIAUYWfl9Vq+mT8/boJFAUzAScgY=
|
||||
github.com/veandco/go-sdl2 v0.4.10/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2 h1:3HA/5qD8Rimxz/y1sLyVaM7ws1dzjXzMt4hOBiwHggo=
|
||||
github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2/go.mod h1:iNfbIyOBN8k3XScMxULbrwYbPsXEAUD0Jb6UwrspQb8=
|
||||
github.com/bloeys/assimp-go v0.4.4 h1:Yn5e/RpE0Oes0YMBy8O7KkwAO4R/RpgrZPJCt08dVIU=
|
||||
github.com/bloeys/assimp-go v0.4.4/go.mod h1:my3yRxT7CfOztmvi+0svmwbaqw0KFrxaHxncoyaEIP0=
|
||||
github.com/bloeys/gglm v0.43.0 h1:ZpOghR3PHfpkigTDh+FqxLsF0gN8CD6s/bWoei6LyxI=
|
||||
github.com/bloeys/gglm v0.43.0/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
|
||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
|
||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/veandco/go-sdl2 v0.4.35 h1:NohzsfageDWGtCd9nf7Pc3sokMK/MOK+UA2QMJARWzQ=
|
||||
github.com/veandco/go-sdl2 v0.4.35/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
|
||||
@ -31,10 +31,11 @@ type mouseWheelState struct {
|
||||
}
|
||||
|
||||
var (
|
||||
keyMap = make(map[sdl.Keycode]*keyState)
|
||||
mouseBtnMap = make(map[int]*mouseBtnState)
|
||||
mouseMotion = mouseMotionState{}
|
||||
mouseWheel = mouseWheelState{}
|
||||
keyMap = make(map[sdl.Keycode]*keyState)
|
||||
mouseBtnMap = make(map[int]*mouseBtnState)
|
||||
mouseMotion = mouseMotionState{}
|
||||
mouseWheel = mouseWheelState{}
|
||||
quitRequested bool
|
||||
)
|
||||
|
||||
func EventLoopStart() {
|
||||
@ -55,6 +56,16 @@ func EventLoopStart() {
|
||||
|
||||
mouseWheel.XDelta = 0
|
||||
mouseWheel.YDelta = 0
|
||||
|
||||
quitRequested = false
|
||||
}
|
||||
|
||||
func HandleQuitEvent(e *sdl.QuitEvent) {
|
||||
quitRequested = true
|
||||
}
|
||||
|
||||
func IsQuitClicked() bool {
|
||||
return quitRequested
|
||||
}
|
||||
|
||||
func HandleKeyboardEvent(e *sdl.KeyboardEvent) {
|
||||
|
||||
17
level/level.go
Executable file
@ -0,0 +1,17 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"github.com/bloeys/nmage/assert"
|
||||
)
|
||||
|
||||
type Level struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewLevel(name string) *Level {
|
||||
|
||||
assert.T(name != "", "Level name can not be empty")
|
||||
return &Level{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
125
materials/material.go
Executable file
@ -0,0 +1,125 @@
|
||||
package materials
|
||||
|
||||
import (
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/bloeys/nmage/logging"
|
||||
"github.com/bloeys/nmage/shaders"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
type Material struct {
|
||||
Name string
|
||||
ShaderProg shaders.ShaderProgram
|
||||
|
||||
DiffuseTex uint32
|
||||
|
||||
UnifLocs map[string]int32
|
||||
AttribLocs map[string]int32
|
||||
}
|
||||
|
||||
func (m *Material) Bind() {
|
||||
|
||||
gl.UseProgram(m.ShaderProg.ID)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, m.DiffuseTex)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
loc, ok := m.AttribLocs[attribName]
|
||||
if ok {
|
||||
return loc
|
||||
}
|
||||
|
||||
loc = gl.GetAttribLocation(m.ShaderProg.ID, gl.Str(attribName+"\x00"))
|
||||
assert.T(loc != -1, "Attribute '"+attribName+"' doesn't exist on material "+m.Name)
|
||||
m.AttribLocs[attribName] = loc
|
||||
return loc
|
||||
}
|
||||
|
||||
func (m *Material) GetUnifLoc(uniformName string) int32 {
|
||||
|
||||
loc, ok := m.UnifLocs[uniformName]
|
||||
if ok {
|
||||
return loc
|
||||
}
|
||||
|
||||
loc = gl.GetUniformLocation(m.ShaderProg.ID, gl.Str(uniformName+"\x00"))
|
||||
assert.T(loc != -1, "Uniform '"+uniformName+"' doesn't exist on material "+m.Name)
|
||||
m.UnifLocs[uniformName] = loc
|
||||
return loc
|
||||
}
|
||||
|
||||
func (m *Material) EnableAttribute(attribName string) {
|
||||
gl.EnableVertexAttribArray(uint32(m.GetAttribLoc(attribName)))
|
||||
}
|
||||
|
||||
func (m *Material) DisableAttribute(attribName string) {
|
||||
gl.DisableVertexAttribArray(uint32(m.GetAttribLoc(attribName)))
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifInt32(uniformName string, val int32) {
|
||||
gl.ProgramUniform1i(m.ShaderProg.ID, m.GetUnifLoc(uniformName), val)
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifFloat32(uniformName string, val float32) {
|
||||
gl.ProgramUniform1f(m.ShaderProg.ID, m.GetUnifLoc(uniformName), val)
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
|
||||
gl.ProgramUniform2fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec2.Data[0])
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
|
||||
gl.ProgramUniform3fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec3.Data[0])
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
|
||||
gl.ProgramUniform4fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, &vec4.Data[0])
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
|
||||
gl.ProgramUniformMatrix2fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat2.Data[0][0])
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
|
||||
gl.ProgramUniformMatrix3fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat3.Data[0][0])
|
||||
}
|
||||
|
||||
func (m *Material) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
|
||||
gl.ProgramUniformMatrix4fv(m.ShaderProg.ID, m.GetUnifLoc(uniformName), 1, false, &mat4.Data[0][0])
|
||||
}
|
||||
|
||||
func (m *Material) Delete() {
|
||||
gl.DeleteProgram(m.ShaderProg.ID)
|
||||
}
|
||||
|
||||
func NewMaterial(matName, shaderPath string) *Material {
|
||||
|
||||
shdrProg, err := shaders.LoadAndCompileCombinedShader(shaderPath)
|
||||
if err != nil {
|
||||
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
|
||||
}
|
||||
|
||||
return &Material{Name: matName, ShaderProg: shdrProg, UnifLocs: make(map[string]int32), AttribLocs: make(map[string]int32)}
|
||||
}
|
||||
|
||||
func NewMaterialSrc(matName string, shaderSrc []byte) *Material {
|
||||
|
||||
shdrProg, err := shaders.LoadAndCompileCombinedShaderSrc(shaderSrc)
|
||||
if err != nil {
|
||||
logging.ErrLog.Fatalln("Failed to create new material. Err: ", err)
|
||||
}
|
||||
|
||||
return &Material{Name: matName, ShaderProg: shdrProg, UnifLocs: make(map[string]int32), AttribLocs: make(map[string]int32)}
|
||||
}
|
||||
186
meshes/mesh.go
Executable file
@ -0,0 +1,186 @@
|
||||
package meshes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/bloeys/assimp-go/asig"
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/assert"
|
||||
"github.com/bloeys/nmage/buffers"
|
||||
)
|
||||
|
||||
type SubMesh struct {
|
||||
BaseVertex int32
|
||||
BaseIndex uint32
|
||||
IndexCount int32
|
||||
}
|
||||
|
||||
type Mesh struct {
|
||||
Name string
|
||||
Buf buffers.Buffer
|
||||
SubMeshes []SubMesh
|
||||
}
|
||||
|
||||
func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh, error) {
|
||||
|
||||
scene, release, err := asig.ImportFile(modelPath, asig.PostProcessTriangulate|postProcessFlags)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to load model. Err: " + err.Error())
|
||||
}
|
||||
defer release()
|
||||
|
||||
if len(scene.Meshes) == 0 {
|
||||
return nil, errors.New("No meshes found in file: " + modelPath)
|
||||
}
|
||||
|
||||
mesh := &Mesh{
|
||||
Name: name,
|
||||
Buf: buffers.NewBuffer(),
|
||||
SubMeshes: make([]SubMesh, 0, 1),
|
||||
}
|
||||
|
||||
// Initial sizes assuming one submesh that has vertex pos+normals+texCoords, and 3 indices per face
|
||||
var vertexBufData []float32 = make([]float32, 0, len(scene.Meshes[0].Vertices)*3*3*2)
|
||||
var indexBufData []uint32 = make([]uint32, 0, len(scene.Meshes[0].Faces)*3)
|
||||
|
||||
for i := 0; i < len(scene.Meshes); i++ {
|
||||
|
||||
sceneMesh := scene.Meshes[i]
|
||||
|
||||
if len(sceneMesh.TexCoords[0]) == 0 {
|
||||
sceneMesh.TexCoords[0] = make([]gglm.Vec3, len(sceneMesh.Vertices))
|
||||
println("Zeroing tex coords for submesh", i)
|
||||
}
|
||||
|
||||
layoutToUse := []buffers.Element{{ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec3}, {ElementType: buffers.DataTypeVec2}}
|
||||
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
|
||||
layoutToUse = append(layoutToUse, buffers.Element{ElementType: buffers.DataTypeVec4})
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
mesh.Buf.SetLayout(layoutToUse...)
|
||||
} else {
|
||||
|
||||
// @NOTE: Require that all submeshes have the same vertex buffer layout
|
||||
firstSubmeshLayout := mesh.Buf.GetLayout()
|
||||
assert.T(len(firstSubmeshLayout) == len(layoutToUse), fmt.Sprintf("Vertex layout of submesh %d does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, firstSubmeshLayout, layoutToUse))
|
||||
|
||||
for i := 0; i < len(firstSubmeshLayout); i++ {
|
||||
if firstSubmeshLayout[i].ElementType != layoutToUse[i].ElementType {
|
||||
panic(fmt.Sprintf("Vertex layout of submesh %d does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, firstSubmeshLayout, layoutToUse))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrs := []arrToInterleave{{V3s: sceneMesh.Vertices}, {V3s: sceneMesh.Normals}, {V2s: v3sToV2s(sceneMesh.TexCoords[0])}}
|
||||
if len(sceneMesh.ColorSets) > 0 && len(sceneMesh.ColorSets[0]) > 0 {
|
||||
arrs = append(arrs, arrToInterleave{V4s: sceneMesh.ColorSets[0]})
|
||||
}
|
||||
|
||||
indices := flattenFaces(sceneMesh.Faces)
|
||||
mesh.SubMeshes = append(mesh.SubMeshes, SubMesh{
|
||||
|
||||
// Index of the vertex to start from (e.g. if index buffer says use vertex 5, and BaseVertex=3, the vertex used will be vertex 8)
|
||||
BaseVertex: int32(len(vertexBufData)*4) / mesh.Buf.Stride,
|
||||
// Which index (in the index buffer) to start from
|
||||
BaseIndex: uint32(len(indexBufData)),
|
||||
// How many indices in this submesh
|
||||
IndexCount: int32(len(indices)),
|
||||
})
|
||||
|
||||
vertexBufData = append(vertexBufData, interleave(arrs...)...)
|
||||
indexBufData = append(indexBufData, indices...)
|
||||
}
|
||||
|
||||
// fmt.Printf("!!! Vertex count: %d; Submeshes: %+v\n", len(vertexBufData)*4/int(mesh.Buf.Stride), mesh.SubMeshes)
|
||||
mesh.Buf.SetData(vertexBufData)
|
||||
mesh.Buf.SetIndexBufData(indexBufData)
|
||||
return mesh, nil
|
||||
}
|
||||
|
||||
func v3sToV2s(v3s []gglm.Vec3) []gglm.Vec2 {
|
||||
|
||||
v2s := make([]gglm.Vec2, len(v3s))
|
||||
for i := 0; i < len(v3s); i++ {
|
||||
v2s[i] = gglm.Vec2{
|
||||
Data: [2]float32{v3s[i].X(), v3s[i].Y()},
|
||||
}
|
||||
}
|
||||
|
||||
return v2s
|
||||
}
|
||||
|
||||
type arrToInterleave struct {
|
||||
V2s []gglm.Vec2
|
||||
V3s []gglm.Vec3
|
||||
V4s []gglm.Vec4
|
||||
}
|
||||
|
||||
func (a *arrToInterleave) get(i int) []float32 {
|
||||
|
||||
assert.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
|
||||
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
|
||||
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
|
||||
|
||||
if len(a.V2s) > 0 {
|
||||
return a.V2s[i].Data[:]
|
||||
} else if len(a.V3s) > 0 {
|
||||
return a.V3s[i].Data[:]
|
||||
} else {
|
||||
return a.V4s[i].Data[:]
|
||||
}
|
||||
}
|
||||
|
||||
func interleave(arrs ...arrToInterleave) []float32 {
|
||||
|
||||
assert.T(len(arrs) > 0, "No input sent to interleave")
|
||||
assert.T(len(arrs[0].V2s) > 0 || len(arrs[0].V3s) > 0 || len(arrs[0].V4s) > 0, "Interleave arrays are empty")
|
||||
|
||||
elementCount := 0
|
||||
if len(arrs[0].V2s) > 0 {
|
||||
elementCount = len(arrs[0].V2s)
|
||||
} else if len(arrs[0].V3s) > 0 {
|
||||
elementCount = len(arrs[0].V3s)
|
||||
} else {
|
||||
elementCount = len(arrs[0].V4s)
|
||||
}
|
||||
|
||||
//Calculate final size of the float buffer
|
||||
totalSize := 0
|
||||
for i := 0; i < len(arrs); i++ {
|
||||
|
||||
assert.T(len(arrs[i].V2s) == elementCount || len(arrs[i].V3s) == elementCount || len(arrs[i].V4s) == elementCount, "Mesh vertex data given to interleave is not the same length")
|
||||
|
||||
if len(arrs[i].V2s) > 0 {
|
||||
totalSize += len(arrs[i].V2s) * 2
|
||||
} else if len(arrs[i].V3s) > 0 {
|
||||
totalSize += len(arrs[i].V3s) * 3
|
||||
} else {
|
||||
totalSize += len(arrs[i].V4s) * 4
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]float32, 0, totalSize)
|
||||
for i := 0; i < elementCount; i++ {
|
||||
for arrToUse := 0; arrToUse < len(arrs); arrToUse++ {
|
||||
out = append(out, arrs[arrToUse].get(i)...)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenFaces(faces []asig.Face) []uint32 {
|
||||
|
||||
assert.T(len(faces[0].Indices) == 3, fmt.Sprintf("Face doesn't have 3 indices. Index count: %v\n", len(faces[0].Indices)))
|
||||
|
||||
uints := make([]uint32, len(faces)*3)
|
||||
for i := 0; i < len(faces); i++ {
|
||||
uints[i*3+0] = uint32(faces[i].Indices[0])
|
||||
uints[i*3+1] = uint32(faces[i].Indices[1])
|
||||
uints[i*3+2] = uint32(faces[i].Indices[2])
|
||||
}
|
||||
|
||||
return uints
|
||||
}
|
||||
73
registry/iterator.go
Executable file
@ -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
|
||||
}
|
||||
131
registry/registry.go
Executable file
@ -0,0 +1,131 @@
|
||||
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 {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
37
registry/registry_handle.go
Executable file
@ -0,0 +1,37 @@
|
||||
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
|
||||
|
||||
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))
|
||||
}
|
||||
44
renderer/rend3dgl/rend3dgl.go
Executable file
@ -0,0 +1,44 @@
|
||||
package rend3dgl
|
||||
|
||||
import (
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/materials"
|
||||
"github.com/bloeys/nmage/meshes"
|
||||
"github.com/bloeys/nmage/renderer"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
|
||||
var _ renderer.Render = &Rend3DGL{}
|
||||
|
||||
type Rend3DGL struct {
|
||||
BoundMesh *meshes.Mesh
|
||||
BoundMat *materials.Material
|
||||
}
|
||||
|
||||
func (r3d *Rend3DGL) Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material) {
|
||||
|
||||
if mesh != r3d.BoundMesh {
|
||||
mesh.Buf.Bind()
|
||||
r3d.BoundMesh = mesh
|
||||
}
|
||||
|
||||
if mat != r3d.BoundMat {
|
||||
mat.Bind()
|
||||
r3d.BoundMat = mat
|
||||
}
|
||||
|
||||
mat.SetUnifMat4("modelMat", &trMat.Mat4)
|
||||
|
||||
for i := 0; i < len(mesh.SubMeshes); i++ {
|
||||
gl.DrawElementsBaseVertexWithOffset(gl.TRIANGLES, mesh.SubMeshes[i].IndexCount, gl.UNSIGNED_INT, uintptr(mesh.SubMeshes[i].BaseIndex), mesh.SubMeshes[i].BaseVertex)
|
||||
}
|
||||
}
|
||||
|
||||
func (r3d *Rend3DGL) FrameEnd() {
|
||||
r3d.BoundMesh = nil
|
||||
r3d.BoundMat = nil
|
||||
}
|
||||
|
||||
func NewRend3DGL() *Rend3DGL {
|
||||
return &Rend3DGL{}
|
||||
}
|
||||
12
renderer/renderer.go
Executable file
@ -0,0 +1,12 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/materials"
|
||||
"github.com/bloeys/nmage/meshes"
|
||||
)
|
||||
|
||||
type Render interface {
|
||||
Draw(mesh *meshes.Mesh, trMat *gglm.TrMat, mat *materials.Material)
|
||||
FrameEnd()
|
||||
}
|
||||
BIN
res/models/chair.fbx
Executable file
@ -1,46 +0,0 @@
|
||||
# Blender v2.92.0 OBJ File: ''
|
||||
# www.blender.org
|
||||
mtllib obj.mtl
|
||||
o Cube
|
||||
v 2.275618 1.000000 0.349413
|
||||
v 3.520138 -1.000000 0.102233
|
||||
v 2.275618 1.000000 0.752820
|
||||
v 3.520138 -1.000000 1.000000
|
||||
v 0.244520 1.000000 0.349413
|
||||
v -1.000000 -1.000000 0.102233
|
||||
v 0.244520 1.000000 0.752820
|
||||
v -1.000000 -1.000000 1.000000
|
||||
vt 0.806168 0.568832
|
||||
vt 0.693832 0.681168
|
||||
vt 0.693832 0.568832
|
||||
vt 0.375000 1.000000
|
||||
vt 0.375000 0.750000
|
||||
vt 0.375000 0.000000
|
||||
vt 0.625000 0.250000
|
||||
vt 0.375000 0.250000
|
||||
vt 0.375000 0.500000
|
||||
vt 0.125000 0.750000
|
||||
vt 0.125000 0.500000
|
||||
vt 0.806168 0.681168
|
||||
vt 0.625000 0.931168
|
||||
vt 0.625000 0.068832
|
||||
vn 0.0000 1.0000 0.0000
|
||||
vn 0.0000 0.1227 0.9924
|
||||
vn -0.8490 0.5283 0.0000
|
||||
vn 0.0000 -1.0000 0.0000
|
||||
vn 0.8490 0.5283 0.0000
|
||||
vn 0.0000 0.1227 -0.9924
|
||||
usemtl Material
|
||||
s off
|
||||
f 5/1/1 3/2/1 1/3/1
|
||||
f 3/2/2 8/4/2 4/5/2
|
||||
f 8/6/3 5/7/3 6/8/3
|
||||
f 2/9/4 8/10/4 6/11/4
|
||||
f 1/3/5 4/5/5 2/9/5
|
||||
f 5/7/6 2/9/6 6/8/6
|
||||
f 5/1/1 7/12/1 3/2/1
|
||||
f 3/2/2 7/13/2 8/4/2
|
||||
f 8/6/3 7/14/3 5/7/3
|
||||
f 2/9/4 4/5/4 8/10/4
|
||||
f 1/3/5 3/2/5 4/5/5
|
||||
f 5/7/6 1/3/6 2/9/6
|
||||
38
res/models/skybox-cube.obj
Executable file
@ -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/tex-cube.fbx
Executable file
52
res/shaders/debug-depth.glsl
Executable file
@ -0,0 +1,52 @@
|
||||
//shader:vertex
|
||||
#version 410
|
||||
|
||||
layout(location=0) in vec3 vertPosIn;
|
||||
layout(location=1) in vec3 vertNormalIn;
|
||||
layout(location=2) in vec2 vertUV0In;
|
||||
layout(location=3) in vec3 vertColorIn;
|
||||
|
||||
out vec3 vertNormal;
|
||||
out vec2 vertUV0;
|
||||
out vec3 vertColor;
|
||||
out vec3 fragPos;
|
||||
|
||||
//MVP = Model View Projection
|
||||
uniform mat4 modelMat;
|
||||
uniform mat4 viewMat;
|
||||
uniform mat4 projMat;
|
||||
|
||||
void main()
|
||||
{
|
||||
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
|
||||
vertUV0 = vertUV0In;
|
||||
vertColor = vertColorIn;
|
||||
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
|
||||
|
||||
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
|
||||
}
|
||||
|
||||
//shader:fragment
|
||||
#version 410
|
||||
|
||||
in vec3 vertColor;
|
||||
in vec3 vertNormal;
|
||||
in vec2 vertUV0;
|
||||
in vec3 fragPos;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform float near = 0.1;
|
||||
uniform float far = 200.0;
|
||||
|
||||
float LinearizeDepth(float depth)
|
||||
{
|
||||
float z = depth * 2.0 - 1.0; // back to NDC
|
||||
return (2.0 * near * far) / (far + near - z * (far - near));
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
float depth = LinearizeDepth(gl_FragCoord.z) / far;
|
||||
fragColor = vec4(vec3(depth), 1.0);
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
#version 410
|
||||
|
||||
uniform sampler2D Texture;
|
||||
|
||||
in vec2 Frag_UV;
|
||||
in vec4 Frag_Color;
|
||||
|
||||
out vec4 Out_Color;
|
||||
|
||||
void main()
|
||||
{
|
||||
Out_Color = vec4(Frag_Color.rgb, Frag_Color.a * texture(Texture, Frag_UV.st).r);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
#version 410
|
||||
|
||||
uniform mat4 ProjMtx;
|
||||
|
||||
in vec2 Position;
|
||||
in vec2 UV;
|
||||
in vec4 Color;
|
||||
|
||||
out vec2 Frag_UV;
|
||||
out vec4 Frag_Color;
|
||||
|
||||
void main()
|
||||
{
|
||||
Frag_UV = UV;
|
||||
Frag_Color = Color;
|
||||
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
#version 410
|
||||
|
||||
in vec3 vertColor;
|
||||
in vec3 vertNormal;
|
||||
in vec3 fragPos;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform float ambientStrength = 0.1;
|
||||
uniform vec3 ambientLightColor = vec3(1, 1, 1);
|
||||
|
||||
uniform vec3 lightPos1;
|
||||
uniform vec3 lightColor1;
|
||||
|
||||
void main()
|
||||
{
|
||||
// vec3 norm = normalize(Normal);
|
||||
vec3 lightDir = normalize(lightPos1 - fragPos);
|
||||
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
|
||||
|
||||
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
|
||||
fragColor = vec4(vertColor * (finalAmbientColor + diffStrength*lightColor1), 1.0);
|
||||
}
|
||||
55
res/shaders/simple.glsl
Executable file
@ -0,0 +1,55 @@
|
||||
//shader:vertex
|
||||
#version 410
|
||||
|
||||
layout(location=0) in vec3 vertPosIn;
|
||||
layout(location=1) in vec3 vertNormalIn;
|
||||
layout(location=2) in vec2 vertUV0In;
|
||||
layout(location=3) in vec3 vertColorIn;
|
||||
|
||||
out vec3 vertNormal;
|
||||
out vec2 vertUV0;
|
||||
out vec3 vertColor;
|
||||
out vec3 fragPos;
|
||||
|
||||
//MVP = Model View Projection
|
||||
uniform mat4 modelMat;
|
||||
uniform mat4 viewMat;
|
||||
uniform mat4 projMat;
|
||||
|
||||
void main()
|
||||
{
|
||||
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
|
||||
vertUV0 = vertUV0In;
|
||||
vertColor = vertColorIn;
|
||||
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
|
||||
|
||||
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
|
||||
}
|
||||
|
||||
//shader:fragment
|
||||
#version 410
|
||||
|
||||
uniform float ambientStrength = 0;
|
||||
uniform vec3 ambientLightColor = vec3(1, 1, 1);
|
||||
|
||||
uniform vec3 lightPos1;
|
||||
uniform vec3 lightColor1;
|
||||
|
||||
uniform sampler2D diffTex;
|
||||
|
||||
in vec3 vertColor;
|
||||
in vec3 vertNormal;
|
||||
in vec2 vertUV0;
|
||||
in vec3 fragPos;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 lightDir = normalize(lightPos1 - fragPos);
|
||||
float diffStrength = max(0.0, dot(normalize(vertNormal), lightDir));
|
||||
|
||||
vec3 finalAmbientColor = ambientLightColor * ambientStrength;
|
||||
vec4 texColor = texture(diffTex, vertUV0);
|
||||
fragColor = vec4(texColor.rgb * vertColor * (finalAmbientColor + diffStrength*lightColor1) , texColor.a);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
#version 410
|
||||
|
||||
in vec3 vertPosIn;
|
||||
in vec3 vertColorIn;
|
||||
in vec3 vertNormalIn;
|
||||
|
||||
out vec3 vertColor;
|
||||
out vec3 vertNormal;
|
||||
out vec3 fragPos;
|
||||
|
||||
//MVP = Model View Projection
|
||||
uniform mat4 modelMat;
|
||||
uniform mat4 viewMat;
|
||||
uniform mat4 projMat;
|
||||
|
||||
void main()
|
||||
{
|
||||
vertColor = vertColorIn;
|
||||
vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn;
|
||||
fragPos = vec3(modelMat * vec4(vertPosIn, 1.0));
|
||||
|
||||
gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0);
|
||||
}
|
||||
33
res/shaders/skybox.glsl
Executable file
@ -0,0 +1,33 @@
|
||||
//shader:vertex
|
||||
#version 410
|
||||
|
||||
layout(location=0) in vec3 vertPosIn;
|
||||
layout(location=1) in vec3 vertNormalIn;
|
||||
layout(location=2) in vec2 vertUV0In;
|
||||
layout(location=3) in vec3 vertColorIn;
|
||||
|
||||
out vec3 vertUV0;
|
||||
|
||||
uniform mat4 viewMat;
|
||||
uniform mat4 projMat;
|
||||
|
||||
void main()
|
||||
{
|
||||
vertUV0 = vec3(vertPosIn.x, vertPosIn.y, -vertPosIn.z);
|
||||
vec4 pos = projMat * viewMat * vec4(vertPosIn, 1.0);
|
||||
gl_Position = pos.xyww;
|
||||
}
|
||||
|
||||
//shader:fragment
|
||||
#version 410
|
||||
|
||||
in vec3 vertUV0;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform samplerCube skybox;
|
||||
|
||||
void main()
|
||||
{
|
||||
fragColor = texture(skybox, vertUV0);
|
||||
}
|
||||
BIN
res/textures/Low poly planet.png
Executable file
|
After Width: | Height: | Size: 468 KiB |
BIN
res/textures/pallete-endesga-64-1x.png
Executable file
|
After Width: | Height: | Size: 355 B |
BIN
res/textures/sb-back.jpg
Executable file
|
After Width: | Height: | Size: 1008 KiB |
BIN
res/textures/sb-bottom.jpg
Executable file
|
After Width: | Height: | Size: 274 KiB |
BIN
res/textures/sb-front.jpg
Executable file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
res/textures/sb-left.jpg
Executable file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
res/textures/sb-right.jpg
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
res/textures/sb-top.jpg
Executable file
|
After Width: | Height: | Size: 338 KiB |
BIN
rsrc_windows_386.syso
Executable file
BIN
rsrc_windows_amd64.syso
Executable file
@ -1,8 +1,6 @@
|
||||
package shaders
|
||||
|
||||
import (
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/buffers"
|
||||
"github.com/bloeys/nmage/logging"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
)
|
||||
@ -37,74 +35,3 @@ func (sp *ShaderProgram) Link() {
|
||||
gl.DeleteShader(sp.FragShaderID)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) Activate() {
|
||||
gl.UseProgram(sp.ID)
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) Deactivate() {
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) GetAttribLoc(attribName string) int32 {
|
||||
return gl.GetAttribLocation(sp.ID, gl.Str(attribName+"\x00"))
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetAttribute(attribName string, bufObj *buffers.BufferObject, buf *buffers.Buffer) {
|
||||
|
||||
bufObj.Activate()
|
||||
buf.Activate()
|
||||
|
||||
attribLoc := sp.GetAttribLoc(attribName)
|
||||
gl.VertexAttribPointer(uint32(attribLoc), buf.ElementCount, buf.ElementType, false, buf.GetSize(), gl.PtrOffset(0))
|
||||
|
||||
bufObj.Activate()
|
||||
buf.Deactivate()
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) EnableAttribute(attribName string) {
|
||||
gl.EnableVertexAttribArray(uint32(sp.GetAttribLoc(attribName)))
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) DisableAttribute(attribName string) {
|
||||
gl.DisableVertexAttribArray(uint32(sp.GetAttribLoc(attribName)))
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifFloat32(uniformName string, val float32) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniform1f(sp.ID, loc, val)
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniform2fv(sp.ID, loc, 1, &vec2.Data[0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniform3fv(sp.ID, loc, 1, &vec3.Data[0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniform4fv(sp.ID, loc, 1, &vec4.Data[0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniformMatrix2fv(sp.ID, loc, 1, false, &mat2.Data[0][0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniformMatrix3fv(sp.ID, loc, 1, false, &mat3.Data[0][0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
|
||||
loc := gl.GetUniformLocation(sp.ID, gl.Str(uniformName+"\x00"))
|
||||
gl.ProgramUniformMatrix4fv(sp.ID, loc, 1, false, &mat4.Data[0][0])
|
||||
}
|
||||
|
||||
func (sp *ShaderProgram) Delete() {
|
||||
gl.DeleteProgram(sp.ID)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package shaders
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
@ -28,14 +29,69 @@ func NewShaderProgram() (ShaderProgram, error) {
|
||||
return ShaderProgram{ID: id}, nil
|
||||
}
|
||||
|
||||
func LoadAndCompilerShader(shaderPath string, shaderType ShaderType) (Shader, error) {
|
||||
func LoadAndCompileCombinedShader(shaderPath string) (ShaderProgram, error) {
|
||||
|
||||
shaderSource, err := os.ReadFile(shaderPath)
|
||||
combinedSource, err := os.ReadFile(shaderPath)
|
||||
if err != nil {
|
||||
logging.ErrLog.Println("Failed to read shader. Err: ", err)
|
||||
return Shader{}, err
|
||||
return ShaderProgram{}, err
|
||||
}
|
||||
|
||||
return LoadAndCompileCombinedShaderSrc(combinedSource)
|
||||
|
||||
}
|
||||
func LoadAndCompileCombinedShaderSrc(shaderSrc []byte) (ShaderProgram, error) {
|
||||
|
||||
shaderSources := bytes.Split(shaderSrc, []byte("//shader:"))
|
||||
if len(shaderSources) == 1 {
|
||||
return ShaderProgram{}, errors.New("failed to read combined shader. Did not find '//shader:vertex' or '//shader:fragment'")
|
||||
}
|
||||
|
||||
shdrProg, err := NewShaderProgram()
|
||||
if err != nil {
|
||||
return ShaderProgram{}, errors.New("failed to create new shader program. Err: " + err.Error())
|
||||
}
|
||||
|
||||
loadedShdrCount := 0
|
||||
for i := 0; i < len(shaderSources); i++ {
|
||||
|
||||
src := shaderSources[i]
|
||||
|
||||
//This can happen when the shader type is at the start of the file
|
||||
if len(bytes.TrimSpace(src)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var shdrType ShaderType
|
||||
if bytes.HasPrefix(src, []byte("vertex")) {
|
||||
src = src[6:]
|
||||
shdrType = VertexShaderType
|
||||
} else if bytes.HasPrefix(src, []byte("fragment")) {
|
||||
src = src[8:]
|
||||
shdrType = FragmentShaderType
|
||||
} else {
|
||||
return ShaderProgram{}, errors.New("unknown shader type. Must be '//shader:vertex' or '//shader:fragment'")
|
||||
}
|
||||
|
||||
shdr, err := CompileShaderOfType(src, shdrType)
|
||||
if err != nil {
|
||||
return ShaderProgram{}, err
|
||||
}
|
||||
|
||||
loadedShdrCount++
|
||||
shdrProg.AttachShader(shdr)
|
||||
}
|
||||
|
||||
if loadedShdrCount == 0 {
|
||||
return ShaderProgram{}, errors.New("no valid shaders found. Please put '//shader:vertex' or '//shader:fragment' before your shaders")
|
||||
}
|
||||
|
||||
shdrProg.Link()
|
||||
return shdrProg, nil
|
||||
}
|
||||
|
||||
func CompileShaderOfType(shaderSource []byte, shaderType ShaderType) (Shader, error) {
|
||||
|
||||
shaderID := gl.CreateShader(uint32(shaderType))
|
||||
if shaderID == 0 {
|
||||
logging.ErrLog.Println("Failed to create shader.")
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
package timing
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dt float32 = 0.01
|
||||
frameStart time.Time
|
||||
startTime time.Time
|
||||
|
||||
//fps calculator vars
|
||||
dtAccum float32 = 1
|
||||
lastElapsedTime uint64 = 0
|
||||
framesSinceLastFPSUpdate uint = 0
|
||||
avgFps float32 = 1
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@ -13,18 +21,42 @@ func Init() {
|
||||
}
|
||||
|
||||
func FrameStarted() {
|
||||
|
||||
frameStart = time.Now()
|
||||
|
||||
//fps stuff
|
||||
dtAccum += dt
|
||||
framesSinceLastFPSUpdate++
|
||||
et := ElapsedTime()
|
||||
if et > lastElapsedTime {
|
||||
avgDT := dtAccum / float32(framesSinceLastFPSUpdate)
|
||||
avgFps = 1 / avgDT
|
||||
|
||||
dtAccum = 0
|
||||
framesSinceLastFPSUpdate = 0
|
||||
}
|
||||
lastElapsedTime = et
|
||||
}
|
||||
|
||||
func FrameEnded() {
|
||||
|
||||
//Calculate new dt
|
||||
dt = float32(time.Since(frameStart).Seconds())
|
||||
if dt == 0 {
|
||||
dt = float32(time.Microsecond.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
//DT is frame deltatime in milliseconds
|
||||
//DT is frame deltatime in seconds
|
||||
func DT() float32 {
|
||||
return dt
|
||||
}
|
||||
|
||||
//GetAvgFPS returns the fps averaged over 1 second
|
||||
func GetAvgFPS() float32 {
|
||||
return avgFps
|
||||
}
|
||||
|
||||
//ElapsedTime is time since game start
|
||||
func ElapsedTime() uint64 {
|
||||
return uint64(time.Since(startTime).Seconds())
|
||||
|
||||
440
ui/imgui/imgui.go
Executable file
@ -0,0 +1,440 @@
|
||||
package nmageimgui
|
||||
|
||||
import (
|
||||
imgui "github.com/AllenDang/cimgui-go"
|
||||
"github.com/bloeys/gglm/gglm"
|
||||
"github.com/bloeys/nmage/materials"
|
||||
"github.com/bloeys/nmage/timing"
|
||||
"github.com/go-gl/gl/v4.1-core/gl"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type ImguiInfo struct {
|
||||
ImCtx imgui.Context
|
||||
|
||||
Mat *materials.Material
|
||||
VaoID uint32
|
||||
VboID uint32
|
||||
IndexBufID uint32
|
||||
TexID uint32
|
||||
}
|
||||
|
||||
func (i *ImguiInfo) FrameStart(winWidth, winHeight float32) {
|
||||
|
||||
// if err := i.ImCtx.SetCurrent(); err != nil {
|
||||
// assert.T(false, "Setting imgui ctx as current failed. Err: "+err.Error())
|
||||
// }
|
||||
|
||||
imIO := imgui.CurrentIO()
|
||||
imIO.SetDisplaySize(imgui.Vec2{X: float32(winWidth), Y: float32(winHeight)})
|
||||
imIO.SetDeltaTime(timing.DT())
|
||||
|
||||
imgui.NewFrame()
|
||||
}
|
||||
|
||||
func (i *ImguiInfo) Render(winWidth, winHeight float32, fbWidth, fbHeight int32) {
|
||||
|
||||
// if err := i.ImCtx.SetCurrent(); err != nil {
|
||||
// assert.T(false, "Setting imgui ctx as current failed. Err: "+err.Error())
|
||||
// }
|
||||
|
||||
imgui.Render()
|
||||
|
||||
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
|
||||
if fbWidth <= 0 || fbHeight <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
drawData := imgui.CurrentDrawData()
|
||||
drawData.ScaleClipRects(imgui.Vec2{
|
||||
X: float32(fbWidth) / float32(winWidth),
|
||||
Y: float32(fbHeight) / float32(winHeight),
|
||||
})
|
||||
|
||||
// Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendEquation(gl.FUNC_ADD)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.Disable(gl.CULL_FACE)
|
||||
gl.Disable(gl.DEPTH_TEST)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.PolygonMode(gl.FRONT_AND_BACK, gl.FILL)
|
||||
|
||||
// Setup viewport, orthographic projection matrix
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right).
|
||||
// DisplayMin is typically (0,0) for single viewport apps.
|
||||
|
||||
i.Mat.Bind()
|
||||
i.Mat.SetUnifInt32("Texture", 0)
|
||||
|
||||
//PERF: only update the ortho matrix on window resize
|
||||
orthoMat := gglm.Ortho(0, float32(winWidth), 0, float32(winHeight), 0, 20)
|
||||
i.Mat.SetUnifMat4("ProjMtx", &orthoMat.Mat4)
|
||||
gl.BindSampler(0, 0) // Rely on combined texture/sampler state.
|
||||
|
||||
// Recreate the VAO every time
|
||||
// (This is to easily allow multiple GL contexts. VAO are not shared among GL contexts, and
|
||||
// we don't track creation/deletion of windows so we don't have an obvious key to use to cache them.)
|
||||
gl.BindVertexArray(i.VaoID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
|
||||
|
||||
vertexSize, vertexOffsetPos, vertexOffsetUv, vertexOffsetCol := imgui.VertexBufferLayout()
|
||||
i.Mat.EnableAttribute("Position")
|
||||
i.Mat.EnableAttribute("UV")
|
||||
i.Mat.EnableAttribute("Color")
|
||||
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Position")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetPos))
|
||||
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("UV")), 2, gl.FLOAT, false, int32(vertexSize), uintptr(vertexOffsetUv))
|
||||
gl.VertexAttribPointerWithOffset(uint32(i.Mat.GetAttribLoc("Color")), 4, gl.UNSIGNED_BYTE, true, int32(vertexSize), uintptr(vertexOffsetCol))
|
||||
|
||||
indexSize := imgui.IndexBufferLayout()
|
||||
drawType := gl.UNSIGNED_SHORT
|
||||
if indexSize == 4 {
|
||||
drawType = gl.UNSIGNED_INT
|
||||
}
|
||||
|
||||
// Draw
|
||||
for _, list := range drawData.CommandLists() {
|
||||
|
||||
vertexBuffer, vertexBufferSize := list.GetVertexBuffer()
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, i.VboID)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, vertexBufferSize, vertexBuffer, gl.STREAM_DRAW)
|
||||
|
||||
indexBuffer, indexBufferSize := list.GetIndexBuffer()
|
||||
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, i.IndexBufID)
|
||||
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, gl.STREAM_DRAW)
|
||||
|
||||
for _, cmd := range list.Commands() {
|
||||
if cmd.HasUserCallback() {
|
||||
cmd.CallUserCallback(list)
|
||||
} else {
|
||||
|
||||
gl.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.ElemCount()), uint32(drawType), gl.PtrOffset(int(cmd.IdxOffset())*indexSize), int32(cmd.VtxOffset()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Reset gl state
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Enable(gl.CULL_FACE)
|
||||
gl.Enable(gl.DEPTH_TEST)
|
||||
}
|
||||
|
||||
func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *imgui.FontConfig, glyphRanges *imgui.GlyphRange) imgui.Font {
|
||||
|
||||
fontConfigToUse := imgui.NewFontConfig()
|
||||
if fontConfig != nil {
|
||||
fontConfigToUse = *fontConfig
|
||||
}
|
||||
|
||||
glyphRangesToUse := imgui.NewGlyphRange()
|
||||
if glyphRanges != nil {
|
||||
glyphRangesToUse = *glyphRanges
|
||||
}
|
||||
|
||||
imIO := imgui.CurrentIO()
|
||||
|
||||
a := imIO.Fonts()
|
||||
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse.Data())
|
||||
pixels, width, height, _ := a.GetTextureDataAsAlpha8()
|
||||
|
||||
gl.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 = `
|
||||
//shader:vertex
|
||||
#version 410
|
||||
|
||||
uniform mat4 ProjMtx;
|
||||
|
||||
in vec2 Position;
|
||||
in vec2 UV;
|
||||
in vec4 Color;
|
||||
|
||||
out vec2 Frag_UV;
|
||||
out vec4 Frag_Color;
|
||||
|
||||
void main()
|
||||
{
|
||||
Frag_UV = UV;
|
||||
Frag_Color = Color;
|
||||
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
|
||||
}
|
||||
|
||||
//shader:fragment
|
||||
#version 410
|
||||
|
||||
uniform sampler2D Texture;
|
||||
|
||||
in vec2 Frag_UV;
|
||||
in vec4 Frag_Color;
|
||||
|
||||
out vec4 Out_Color;
|
||||
|
||||
void main()
|
||||
{
|
||||
Out_Color = vec4(Frag_Color.rgb, Frag_Color.a * texture(Texture, Frag_UV.st).r);
|
||||
}
|
||||
`
|
||||
|
||||
func NewImGui() ImguiInfo {
|
||||
|
||||
imguiInfo := ImguiInfo{
|
||||
ImCtx: imgui.CreateContext(),
|
||||
Mat: materials.NewMaterialSrc("ImGUI Mat", []byte(imguiShdrSrc)),
|
||||
}
|
||||
|
||||
io := imgui.CurrentIO()
|
||||
io.SetConfigFlags(io.ConfigFlags() | imgui.ConfigFlagsDockingEnable)
|
||||
io.SetBackendFlags(io.BackendFlags() | imgui.BackendFlagsRendererHasVtxOffset)
|
||||
|
||||
gl.GenVertexArrays(1, &imguiInfo.VaoID)
|
||||
gl.GenBuffers(1, &imguiInfo.VboID)
|
||||
gl.GenBuffers(1, &imguiInfo.IndexBufID)
|
||||
gl.GenTextures(1, &imguiInfo.TexID)
|
||||
|
||||
// Upload font to gpu
|
||||
gl.BindTexture(gl.TEXTURE_2D, imguiInfo.TexID)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
|
||||
pixels, width, height, _ := io.Fonts().GetTextureDataAsAlpha8()
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(width), int32(height), 0, gl.RED, gl.UNSIGNED_BYTE, pixels)
|
||||
|
||||
// Store our identifier
|
||||
io.Fonts().SetTexID(imgui.TextureID(uintptr(imguiInfo.TexID)))
|
||||
|
||||
//Shader attributes
|
||||
imguiInfo.Mat.Bind()
|
||||
imguiInfo.Mat.EnableAttribute("Position")
|
||||
imguiInfo.Mat.EnableAttribute("UV")
|
||||
imguiInfo.Mat.EnableAttribute("Color")
|
||||
imguiInfo.Mat.UnBind()
|
||||
|
||||
return imguiInfo
|
||||
}
|
||||
|
||||
func SdlScancodeToImGuiKey(scancode sdl.Scancode) imgui.Key {
|
||||
|
||||
switch scancode {
|
||||
case sdl.SCANCODE_TAB:
|
||||
return imgui.KeyTab
|
||||
case sdl.SCANCODE_LEFT:
|
||||
return imgui.KeyLeftArrow
|
||||
case sdl.SCANCODE_RIGHT:
|
||||
return imgui.KeyRightArrow
|
||||
case sdl.SCANCODE_UP:
|
||||
return imgui.KeyUpArrow
|
||||
case sdl.SCANCODE_DOWN:
|
||||
return imgui.KeyDownArrow
|
||||
case sdl.SCANCODE_PAGEUP:
|
||||
return imgui.KeyPageUp
|
||||
case sdl.SCANCODE_PAGEDOWN:
|
||||
return imgui.KeyPageDown
|
||||
case sdl.SCANCODE_HOME:
|
||||
return imgui.KeyHome
|
||||
case sdl.SCANCODE_END:
|
||||
return imgui.KeyEnd
|
||||
case sdl.SCANCODE_INSERT:
|
||||
return imgui.KeyInsert
|
||||
case sdl.SCANCODE_DELETE:
|
||||
return imgui.KeyDelete
|
||||
case sdl.SCANCODE_BACKSPACE:
|
||||
return imgui.KeyBackspace
|
||||
case sdl.SCANCODE_SPACE:
|
||||
return imgui.KeySpace
|
||||
case sdl.SCANCODE_RETURN:
|
||||
return imgui.KeyEnter
|
||||
case sdl.SCANCODE_ESCAPE:
|
||||
return imgui.KeyEscape
|
||||
case sdl.SCANCODE_APOSTROPHE:
|
||||
return imgui.KeyApostrophe
|
||||
case sdl.SCANCODE_COMMA:
|
||||
return imgui.KeyComma
|
||||
case sdl.SCANCODE_MINUS:
|
||||
return imgui.KeyMinus
|
||||
case sdl.SCANCODE_PERIOD:
|
||||
return imgui.KeyPeriod
|
||||
case sdl.SCANCODE_SLASH:
|
||||
return imgui.KeySlash
|
||||
case sdl.SCANCODE_SEMICOLON:
|
||||
return imgui.KeySemicolon
|
||||
case sdl.SCANCODE_EQUALS:
|
||||
return imgui.KeyEqual
|
||||
case sdl.SCANCODE_LEFTBRACKET:
|
||||
return imgui.KeyLeftBracket
|
||||
case sdl.SCANCODE_BACKSLASH:
|
||||
return imgui.KeyBackslash
|
||||
case sdl.SCANCODE_RIGHTBRACKET:
|
||||
return imgui.KeyRightBracket
|
||||
case sdl.SCANCODE_GRAVE:
|
||||
return imgui.KeyGraveAccent
|
||||
case sdl.SCANCODE_CAPSLOCK:
|
||||
return imgui.KeyCapsLock
|
||||
case sdl.SCANCODE_SCROLLLOCK:
|
||||
return imgui.KeyScrollLock
|
||||
case sdl.SCANCODE_NUMLOCKCLEAR:
|
||||
return imgui.KeyNumLock
|
||||
case sdl.SCANCODE_PRINTSCREEN:
|
||||
return imgui.KeyPrintScreen
|
||||
case sdl.SCANCODE_PAUSE:
|
||||
return imgui.KeyPause
|
||||
case sdl.SCANCODE_KP_0:
|
||||
return imgui.KeyKeypad0
|
||||
case sdl.SCANCODE_KP_1:
|
||||
return imgui.KeyKeypad1
|
||||
case sdl.SCANCODE_KP_2:
|
||||
return imgui.KeyKeypad2
|
||||
case sdl.SCANCODE_KP_3:
|
||||
return imgui.KeyKeypad3
|
||||
case sdl.SCANCODE_KP_4:
|
||||
return imgui.KeyKeypad4
|
||||
case sdl.SCANCODE_KP_5:
|
||||
return imgui.KeyKeypad5
|
||||
case sdl.SCANCODE_KP_6:
|
||||
return imgui.KeyKeypad6
|
||||
case sdl.SCANCODE_KP_7:
|
||||
return imgui.KeyKeypad7
|
||||
case sdl.SCANCODE_KP_8:
|
||||
return imgui.KeyKeypad8
|
||||
case sdl.SCANCODE_KP_9:
|
||||
return imgui.KeyKeypad9
|
||||
case sdl.SCANCODE_KP_PERIOD:
|
||||
return imgui.KeyKeypadDecimal
|
||||
case sdl.SCANCODE_KP_DIVIDE:
|
||||
return imgui.KeyKeypadDivide
|
||||
case sdl.SCANCODE_KP_MULTIPLY:
|
||||
return imgui.KeyKeypadMultiply
|
||||
case sdl.SCANCODE_KP_MINUS:
|
||||
return imgui.KeyKeypadSubtract
|
||||
case sdl.SCANCODE_KP_PLUS:
|
||||
return imgui.KeyKeypadAdd
|
||||
case sdl.SCANCODE_KP_ENTER:
|
||||
return imgui.KeyKeypadEnter
|
||||
case sdl.SCANCODE_KP_EQUALS:
|
||||
return imgui.KeyKeypadEqual
|
||||
case sdl.SCANCODE_LSHIFT:
|
||||
return imgui.KeyLeftShift
|
||||
case sdl.SCANCODE_LCTRL:
|
||||
return imgui.KeyLeftCtrl
|
||||
case sdl.SCANCODE_LALT:
|
||||
return imgui.KeyLeftAlt
|
||||
case sdl.SCANCODE_LGUI:
|
||||
return imgui.KeyLeftSuper
|
||||
case sdl.SCANCODE_RSHIFT:
|
||||
return imgui.KeyRightShift
|
||||
case sdl.SCANCODE_RCTRL:
|
||||
return imgui.KeyRightCtrl
|
||||
case sdl.SCANCODE_RALT:
|
||||
return imgui.KeyRightAlt
|
||||
case sdl.SCANCODE_RGUI:
|
||||
return imgui.KeyRightSuper
|
||||
case sdl.SCANCODE_MENU:
|
||||
return imgui.KeyMenu
|
||||
case sdl.SCANCODE_0:
|
||||
return imgui.Key0
|
||||
case sdl.SCANCODE_1:
|
||||
return imgui.Key1
|
||||
case sdl.SCANCODE_2:
|
||||
return imgui.Key2
|
||||
case sdl.SCANCODE_3:
|
||||
return imgui.Key3
|
||||
case sdl.SCANCODE_4:
|
||||
return imgui.Key4
|
||||
case sdl.SCANCODE_5:
|
||||
return imgui.Key5
|
||||
case sdl.SCANCODE_6:
|
||||
return imgui.Key6
|
||||
case sdl.SCANCODE_7:
|
||||
return imgui.Key7
|
||||
case sdl.SCANCODE_8:
|
||||
return imgui.Key8
|
||||
case sdl.SCANCODE_9:
|
||||
return imgui.Key9
|
||||
case sdl.SCANCODE_A:
|
||||
return imgui.KeyA
|
||||
case sdl.SCANCODE_B:
|
||||
return imgui.KeyB
|
||||
case sdl.SCANCODE_C:
|
||||
return imgui.KeyC
|
||||
case sdl.SCANCODE_D:
|
||||
return imgui.KeyD
|
||||
case sdl.SCANCODE_E:
|
||||
return imgui.KeyE
|
||||
case sdl.SCANCODE_F:
|
||||
return imgui.KeyF
|
||||
case sdl.SCANCODE_G:
|
||||
return imgui.KeyG
|
||||
case sdl.SCANCODE_H:
|
||||
return imgui.KeyH
|
||||
case sdl.SCANCODE_I:
|
||||
return imgui.KeyI
|
||||
case sdl.SCANCODE_J:
|
||||
return imgui.KeyJ
|
||||
case sdl.SCANCODE_K:
|
||||
return imgui.KeyK
|
||||
case sdl.SCANCODE_L:
|
||||
return imgui.KeyL
|
||||
case sdl.SCANCODE_M:
|
||||
return imgui.KeyM
|
||||
case sdl.SCANCODE_N:
|
||||
return imgui.KeyN
|
||||
case sdl.SCANCODE_O:
|
||||
return imgui.KeyO
|
||||
case sdl.SCANCODE_P:
|
||||
return imgui.KeyP
|
||||
case sdl.SCANCODE_Q:
|
||||
return imgui.KeyQ
|
||||
case sdl.SCANCODE_R:
|
||||
return imgui.KeyR
|
||||
case sdl.SCANCODE_S:
|
||||
return imgui.KeyS
|
||||
case sdl.SCANCODE_T:
|
||||
return imgui.KeyT
|
||||
case sdl.SCANCODE_U:
|
||||
return imgui.KeyU
|
||||
case sdl.SCANCODE_V:
|
||||
return imgui.KeyV
|
||||
case sdl.SCANCODE_W:
|
||||
return imgui.KeyW
|
||||
case sdl.SCANCODE_X:
|
||||
return imgui.KeyX
|
||||
case sdl.SCANCODE_Y:
|
||||
return imgui.KeyY
|
||||
case sdl.SCANCODE_Z:
|
||||
return imgui.KeyZ
|
||||
case sdl.SCANCODE_F1:
|
||||
return imgui.KeyF1
|
||||
case sdl.SCANCODE_F2:
|
||||
return imgui.KeyF2
|
||||
case sdl.SCANCODE_F3:
|
||||
return imgui.KeyF3
|
||||
case sdl.SCANCODE_F4:
|
||||
return imgui.KeyF4
|
||||
case sdl.SCANCODE_F5:
|
||||
return imgui.KeyF5
|
||||
case sdl.SCANCODE_F6:
|
||||
return imgui.KeyF6
|
||||
case sdl.SCANCODE_F7:
|
||||
return imgui.KeyF7
|
||||
case sdl.SCANCODE_F8:
|
||||
return imgui.KeyF8
|
||||
case sdl.SCANCODE_F9:
|
||||
return imgui.KeyF9
|
||||
case sdl.SCANCODE_F10:
|
||||
return imgui.KeyF10
|
||||
case sdl.SCANCODE_F11:
|
||||
return imgui.KeyF11
|
||||
case sdl.SCANCODE_F12:
|
||||
return imgui.KeyF12
|
||||
default:
|
||||
return imgui.KeyNone
|
||||
}
|
||||
}
|
||||