Compare commits

...

41 Commits

Author SHA1 Message Date
f5a5d72cc4 Working mac arm+mac x86+windows x86 builds 2024-09-15 18:48:20 +04:00
1bec97b128 test 2024-09-15 18:41:31 +04:00
dea2ac965f test 2024-09-15 18:39:49 +04:00
93b5f08352 test 2024-09-15 18:35:55 +04:00
34e19d9c66 Test 2024-09-15 18:27:03 +04:00
372d9ae6b7 Test 2024-09-15 18:25:18 +04:00
befc78c628 Correct windows action 2024-09-15 18:22:44 +04:00
98f8a96bb7 Build on commit 2024-09-15 18:13:55 +04:00
e767f32f2f Update windows gh action 2024-09-15 18:12:53 +04:00
6d94efbf97 Update actions 2024-09-15 17:47:58 +04:00
28f543a579 Update action 2024-09-15 17:35:40 +04:00
2a73a12885 Update action 2024-09-15 17:34:23 +04:00
e4199b8d30 Update imgui 2024-09-15 17:20:37 +04:00
38248822e2 Update to go 1.23 2024-09-15 16:33:22 +04:00
5c98903723 Add all gl BufUsage values+support bufusage in ubo 2024-09-15 16:29:54 +04:00
3cdd40f0a2 Remove test log 2024-09-15 16:18:59 +04:00
9dccb23613 Move ambient color to lightubo 2024-09-15 16:18:03 +04:00
5dfdea9a7b Move spotlights to ubo 2024-09-15 16:09:37 +04:00
bcb46d1699 Fix ubo alignment+Move point lights to ubo 2024-09-15 15:51:26 +04:00
91807a4093 Merge remote-tracking branch 'refs/remotes/origin/dev' into dev 2024-09-15 12:36:32 +04:00
09231c5ebd Update ubo.SetStruct to handle nested struct arrays 2024-09-15 12:33:34 +04:00
0e98dc85f5 Update ubo.SetStruct to handle nested structs 2024-09-15 12:31:26 +04:00
f2b757c606 Move dirLight&camPos&projViewMat to ubos 2024-09-15 08:40:05 +04:00
3be4ad9c45 Optimize 2024-09-14 19:41:39 +04:00
bbc8652292 What if we could have ubos with nested structs 2024-09-14 18:38:59 +04:00
0d34e0fe6e Ubos with nested structs slowly getting there 2024-06-06 04:59:40 +04:00
870653019c Support ubo matrix arrays+fix ubo matrix field bug 2024-05-26 21:14:33 +04:00
79cb6805c4 Scalar uniform array in uniform buffers 2024-05-26 15:03:00 +04:00
ff7fe4e531 Struct to ubo support 2024-05-26 12:55:58 +04:00
cb20e8ba8b Initial uniform buffers implementation 2024-05-23 07:57:46 +04:00
9e6fdacb48 Much nicer point lights! 2024-05-14 09:07:54 +04:00
f13db47918 Much nicer point light formulas 2024-05-14 07:36:14 +04:00
dcfe254052 Stop sudden camera snaps+nicer debug window 2024-05-14 06:25:39 +04:00
1d71715cb4 Make const value naming upper snake case 2024-05-13 05:35:53 +04:00
581d17d1d9 Frame time graph 2024-05-13 05:18:21 +04:00
3795a7123f Ensure renderer calls aren't virtual 2024-05-13 04:57:16 +04:00
5aa0f41085 Internal material func rename 2024-05-13 04:36:53 +04:00
c782e8c312 Get rid of allocations on SetUniform calls, allowing us to pass ref again 2024-05-13 04:33:54 +04:00
f0a12879f8 Add todo 2024-05-13 03:45:16 +04:00
6ea08e9826 Get rid of more pointers to make allocs predictable 2024-05-13 03:42:52 +04:00
83c6f635e5 Show fps in debug window 2024-05-13 03:21:47 +04:00
16 changed files with 1615 additions and 395 deletions

View File

@ -1,24 +1,69 @@
name: build-nmage
on:
push:
branches:
- dev
create:
workflow_dispatch:
jobs:
build-nmage-macos:
runs-on: macos-12
steps:
build-nmage-windows:
runs-on: windows-latest
steps:
- name: Install golang
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: '>=1.22'
go-version: ">=1.23"
- name: Install assimp-go dll
run: |
Invoke-WebRequest -Uri "https://github.com/bloeys/assimp-go/releases/download/v0.4.2/libassimp-5.dll" -OutFile "C:\Windows\System32\libassimp-5.dll"
- name: Download and setup SDL2
run: |
Invoke-WebRequest -Uri "https://github.com/libsdl-org/SDL/releases/download/release-2.30.7/SDL2-devel-2.30.7-mingw.zip" -OutFile "SDL2.zip"
Expand-Archive -Path "SDL2.zip" -DestinationPath "SDL2"
Copy-Item -Path "SDL2\SDL2-2.30.7\x86_64-w64-mingw32" -Destination "C:\mingw64" -Recurse -Force
- name: Clone nmage
run: git clone https://github.com/bloeys/nmage
- name: Build nmage
working-directory: nmage
run: go build .
build-nmage-macos:
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Based on: https://github.com/actions/runner-images?tab=readme-ov-file#available-images
os:
- macos-13 # x86
- macos-14 # Arm
steps:
- name: Install golang
uses: actions/setup-go@v5
with:
go-version: ">=1.23"
- name: Determine architecture
id: arch
run: |
if [ "$(uname -m)" = "arm64" ]; then
echo "arch=arm64" >> "$GITHUB_OUTPUT"
else
echo "arch=amd64" >> "$GITHUB_OUTPUT"
fi
- 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
run: sudo mkdir -p /usr/local/lib && sudo wget https://github.com/bloeys/assimp-go/releases/download/v0.4.2/libassimp_darwin_${{ steps.arch.outputs.arch }}.dylib -O /usr/local/lib/libassimp.5.dylib
- name: Install SDL2
run: brew install sdl2{,_image,_mixer,_ttf,_gfx} pkg-config
run: brew install sdl2{,_image,_ttf,_gfx} pkg-config
- name: Clone nmage
run: git clone https://github.com/bloeys/nmage

View File

@ -9,25 +9,48 @@ import (
type BufUsage int
// Full docs for buffer usage can be found here: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBufferData.xhtml
const (
BufUsage_Unknown BufUsage = iota
//Buffer is set only once and used many times
BufUsage_Static
BufUsage_Static_Draw
//Buffer is changed a lot and used many times
BufUsage_Dynamic
BufUsage_Dynamic_Draw
//Buffer is set only once and used by the GPU at most a few times
BufUsage_Stream
BufUsage_Stream_Draw
BufUsage_Static_Read
BufUsage_Dynamic_Read
BufUsage_Stream_Read
BufUsage_Static_Copy
BufUsage_Dynamic_Copy
BufUsage_Stream_Copy
)
func (b BufUsage) ToGL() uint32 {
switch b {
case BufUsage_Static:
case BufUsage_Static_Draw:
return gl.STATIC_DRAW
case BufUsage_Dynamic:
case BufUsage_Dynamic_Draw:
return gl.DYNAMIC_DRAW
case BufUsage_Stream:
case BufUsage_Stream_Draw:
return gl.STREAM_DRAW
case BufUsage_Static_Read:
return gl.STATIC_READ
case BufUsage_Dynamic_Read:
return gl.DYNAMIC_READ
case BufUsage_Stream_Read:
return gl.STREAM_READ
case BufUsage_Static_Copy:
return gl.STATIC_COPY
case BufUsage_Dynamic_Copy:
return gl.DYNAMIC_COPY
case BufUsage_Stream_Copy:
return gl.STREAM_COPY
}
assert.T(false, fmt.Sprintf("Unexpected BufUsage value '%v'", b))

View File

@ -1,9 +1,8 @@
package buffers
import (
"fmt"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
@ -14,10 +13,11 @@ type Element struct {
}
// ElementType is the type of an element thats makes up a buffer (e.g. Vec3)
type ElementType int
type ElementType uint8
const (
DataTypeUnknown ElementType = iota
DataTypeUint32
DataTypeInt32
DataTypeFloat32
@ -25,35 +25,54 @@ const (
DataTypeVec2
DataTypeVec3
DataTypeVec4
DataTypeMat2
DataTypeMat3
DataTypeMat4
DataTypeStruct
)
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:
fallthrough
case DataTypeMat2:
fallthrough
case DataTypeMat3:
fallthrough
case DataTypeMat4:
return gl.FLOAT
case DataTypeStruct:
logging.ErrLog.Fatalf("ElementType.GLType of DataTypeStruct is not supported")
return 0
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
//CompSize returns the size in bytes for one component of the type (e.g. for Vec2 its 4)
// CompSize returns the size in bytes for one component of the type (e.g. for Vec2 its 4).
// Bools return 1, although in layout=std140 its 4
func (dt ElementType) CompSize() int32 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
@ -65,10 +84,20 @@ func (dt ElementType) CompSize() int32 {
case DataTypeVec3:
fallthrough
case DataTypeVec4:
fallthrough
case DataTypeMat2:
fallthrough
case DataTypeMat3:
fallthrough
case DataTypeMat4:
return 4
case DataTypeStruct:
logging.ErrLog.Fatalf("ElementType.CompSize of DataTypeStruct is not supported")
return 0
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
@ -91,8 +120,19 @@ func (dt ElementType) CompCount() int32 {
case DataTypeVec4:
return 4
case DataTypeMat2:
return 2 * 2
case DataTypeMat3:
return 3 * 3
case DataTypeMat4:
return 4 * 4
case DataTypeStruct:
logging.ErrLog.Fatalf("ElementType.CompCount of DataTypeStruct is not supported")
return 0
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
@ -101,6 +141,7 @@ func (dt ElementType) CompCount() int32 {
func (dt ElementType) Size() int32 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
@ -115,8 +156,123 @@ func (dt ElementType) Size() int32 {
case DataTypeVec4:
return 4 * 4
case DataTypeMat2:
return 2 * 2 * 4
case DataTypeMat3:
return 3 * 3 * 4
case DataTypeMat4:
return 4 * 4 * 4
case DataTypeStruct:
logging.ErrLog.Fatalf("ElementType.Size of DataTypeStruct is not supported")
return 0
default:
assert.T(false, fmt.Sprintf("Unknown data type passed. DataType '%v'", dt))
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
func (dt ElementType) GlStd140SizeBytes() uint8 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
fallthrough
case DataTypeInt32:
return 4
case DataTypeVec2:
return 4 * 2
case DataTypeVec3:
return 4 * 3
case DataTypeVec4:
return 4 * 4
// Matrices follow: (vec4Alignment) * numColumns
case DataTypeMat2:
return 2 * 2 * 4
case DataTypeMat3:
return 3 * 3 * 4
case DataTypeMat4:
return 4 * 4 * 4
case DataTypeStruct:
logging.ErrLog.Fatalf("ElementType.GlStd140SizeBytes of DataTypeStruct is not supported")
return 0
default:
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
func (dt ElementType) GlStd140AlignmentBoundary() uint16 {
switch dt {
case DataTypeUint32:
fallthrough
case DataTypeFloat32:
fallthrough
case DataTypeInt32:
return 4
case DataTypeVec2:
return 8
case DataTypeVec3:
fallthrough
case DataTypeVec4:
fallthrough
case DataTypeMat2:
fallthrough
case DataTypeMat3:
fallthrough
case DataTypeMat4:
fallthrough
case DataTypeStruct:
return 16
default:
assert.T(false, "Unknown data type passed. DataType '%d'", dt)
return 0
}
}
func (dt ElementType) String() string {
switch dt {
case DataTypeUint32:
return "uint32"
case DataTypeFloat32:
return "float32"
case DataTypeInt32:
return "int32"
case DataTypeVec2:
return "Vec2"
case DataTypeVec3:
return "Vec3"
case DataTypeVec4:
return "Vec4"
case DataTypeMat2:
return "Mat2"
case DataTypeMat3:
return "Mat3"
case DataTypeMat4:
return "Mat4"
case DataTypeStruct:
return "Struct"
default:
return "Unknown"
}
}

View File

@ -27,9 +27,9 @@ func (ib *IndexBuffer) SetData(values []uint32) {
ib.IndexBufCount = int32(len(values))
if sizeInBytes == 0 {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static.ToGL())
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, 0, gl.Ptr(nil), BufUsage_Static_Draw.ToGL())
} else {
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static.ToGL())
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, sizeInBytes, gl.Ptr(&values[0]), BufUsage_Static_Draw.ToGL())
}
}

747
buffers/uniform_buffer.go Executable file
View File

@ -0,0 +1,747 @@
package buffers
import (
"math"
"reflect"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/logging"
"github.com/go-gl/gl/v4.1-core/gl"
)
type UniformBufferFieldInput struct {
Id uint16
Type ElementType
// Count should be set in case this field is an array of type `[Count]Type`.
// Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field.
Count uint16
// Subfields is used when type is a struct, in which case it holds the fields of the struct.
// Ids do not have to be unique across structs.
Subfields []UniformBufferFieldInput
}
type UniformBufferField struct {
Id uint16
AlignedOffset uint16
// Count should be set in case this field is an array of type `[Count]Type`.
// Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field.
Count uint16
Type ElementType
// Subfields is used when type is a struct, in which case it holds the fields of the struct.
// Ids do not have to be unique across structs.
Subfields []UniformBufferField
}
type UniformBuffer struct {
Id uint32
// Size is the allocated memory in bytes on the GPU for this uniform buffer
Size uint32
Fields []UniformBufferField
}
func (ub *UniformBuffer) Bind() {
gl.BindBuffer(gl.UNIFORM_BUFFER, ub.Id)
}
func (ub *UniformBuffer) UnBind() {
gl.BindBuffer(gl.UNIFORM_BUFFER, 0)
}
func (ub *UniformBuffer) SetBindPoint(bindPointIndex uint32) {
gl.BindBufferBase(gl.UNIFORM_BUFFER, bindPointIndex, ub.Id)
}
func addUniformBufferFieldsToArray(startAlignedOffset uint16, arrayToAddTo *[]UniformBufferField, fieldsToAdd []UniformBufferFieldInput) (totalSize uint32) {
if len(fieldsToAdd) == 0 {
return 0
}
// This function is recursive so only size the array once
if cap(*arrayToAddTo) == 0 {
*arrayToAddTo = make([]UniformBufferField, 0, len(fieldsToAdd))
}
var alignedOffset uint16 = 0
fieldIdToTypeMap := make(map[uint16]ElementType, len(fieldsToAdd))
for i := 0; i < len(fieldsToAdd); i++ {
f := fieldsToAdd[i]
if f.Count == 0 {
f.Count = 1
}
existingFieldType, ok := fieldIdToTypeMap[f.Id]
assert.T(!ok, "Uniform buffer field id is reused within the same uniform buffer. FieldId=%d was first used on a field with type=%s and then used on a different field with type=%s\n", f.Id, existingFieldType.String(), f.Type.String())
// To understand this take an example. Say we have a total offset of 100 and we are adding a vec4.
// Vec4s must be aligned to a 16 byte boundary but 100 is not (100 % 16 != 0).
//
// To fix this, we take the alignment error which is alignErr=100 % 16=4, but this is error to the nearest
// boundary, which is below the offset.
//
// To get the nearest boundary larger than the offset we can:
// offset + (boundary - alignErr) == 100 + (16 - 4) == 112; 112 % 16 == 0, meaning its a boundary
//
// Note that arrays of scalars/vectors are always aligned to 16 bytes, like a vec4
//
// Official spec and full details in subsection 'Standard Uniform Block Layout' at http://www.opengl.org/registry/specs/ARB/uniform_buffer_object.txt
var alignmentBoundary uint16 = 16
if f.Count == 1 {
alignmentBoundary = f.Type.GlStd140AlignmentBoundary()
}
alignmentError := alignedOffset % alignmentBoundary
if alignmentError != 0 {
alignedOffset += alignmentBoundary - alignmentError
}
newField := UniformBufferField{Id: f.Id, Type: f.Type, AlignedOffset: startAlignedOffset + alignedOffset, Count: f.Count}
*arrayToAddTo = append(*arrayToAddTo, newField)
// Prepare aligned offset for the next field.
//
// Matrices are treated as an array of column vectors, where each column is a vec4,
// that's why we have a multiplier depending on how many columns we have when calculating
// the offset
multiplier := uint16(1)
if f.Type == DataTypeMat2 {
multiplier = 2
} else if f.Type == DataTypeMat3 {
multiplier = 3
} else if f.Type == DataTypeMat4 {
multiplier = 4
}
if f.Type == DataTypeStruct {
subfieldsAlignedOffset := uint16(addUniformBufferFieldsToArray(startAlignedOffset+alignedOffset, arrayToAddTo, f.Subfields))
// Pad structs to 16 byte boundary
padTo16Boundary(&subfieldsAlignedOffset)
alignedOffset += subfieldsAlignedOffset * f.Count
} else {
// Elements advance the alignedOffset by their actual byte size.
// Aligned offset is padded if the place its at is not aligned to the boundary required by the next element.
//
// The exception is structs, because fields after a struct field are always aligned at a 16 byte boundary.
//
// For example, a vec3 starting at offset 80, taking 12 bytes, would put the aligned offset at 92.
// If the next element is a float32 (alignment boundary = 4) then no padding is required and
// the float will start at 92 and end at 96.
// However, if the element after the vec3 is a vec3 (alignment boundary = 16), then it would require
// a padding of 4 bytes so that it can start at 96, which is aligned to 16. In this case the second vec3
// would start at 96 and end at 96+12=108.
alignedOffset = newField.AlignedOffset + uint16(f.Type.GlStd140SizeBytes())*f.Count*multiplier - startAlignedOffset
}
}
return uint32(alignedOffset)
}
func padTo16Boundary[T uint16 | int | int32](val *T) {
alignmentError := *val % 16
if alignmentError != 0 {
*val += 16 - alignmentError
}
}
func (ub *UniformBuffer) getField(fieldId uint16, fieldType ElementType) UniformBufferField {
for i := 0; i < len(ub.Fields); i++ {
f := ub.Fields[i]
if f.Id != fieldId {
continue
}
assert.T(f.Type == fieldType, "Uniform buffer field id is reused within the same uniform buffer. FieldId=%d was first used on a field with type=%v, but is now being used on a field with type=%v\n", fieldId, f.Type.String(), fieldType.String())
return f
}
logging.ErrLog.Panicf("couldn't find uniform buffer field of id=%d and type=%s\n", fieldId, fieldType.String())
return UniformBufferField{}
}
func (ub *UniformBuffer) SetInt32(fieldId uint16, val int32) {
f := ub.getField(fieldId, DataTypeInt32)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}
func (ub *UniformBuffer) SetUint32(fieldId uint16, val uint32) {
f := ub.getField(fieldId, DataTypeUint32)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}
func (ub *UniformBuffer) SetFloat32(fieldId uint16, val float32) {
f := ub.getField(fieldId, DataTypeFloat32)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}
func (ub *UniformBuffer) SetVec2(fieldId uint16, val *gglm.Vec2) {
f := ub.getField(fieldId, DataTypeVec2)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*2, gl.Ptr(&val.Data[0]))
}
func (ub *UniformBuffer) SetVec3(fieldId uint16, val *gglm.Vec3) {
f := ub.getField(fieldId, DataTypeVec3)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*3, gl.Ptr(&val.Data[0]))
}
func (ub *UniformBuffer) SetVec4(fieldId uint16, val *gglm.Vec4) {
f := ub.getField(fieldId, DataTypeVec4)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*4, gl.Ptr(&val.Data[0]))
}
func (ub *UniformBuffer) SetMat2(fieldId uint16, val *gglm.Mat2) {
f := ub.getField(fieldId, DataTypeMat2)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*4, gl.Ptr(&val.Data[0][0]))
}
func (ub *UniformBuffer) SetMat3(fieldId uint16, val *gglm.Mat3) {
f := ub.getField(fieldId, DataTypeMat3)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*9, gl.Ptr(&val.Data[0][0]))
}
func (ub *UniformBuffer) SetMat4(fieldId uint16, val *gglm.Mat4) {
f := ub.getField(fieldId, DataTypeMat4)
gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*16, gl.Ptr(&val.Data[0][0]))
}
func (ub *UniformBuffer) SetStruct(inputStruct any) {
setStruct(ub.Fields, make([]byte, ub.Size), inputStruct, 1000_000, false, 0)
}
func setStruct(fields []UniformBufferField, buf []byte, inputStruct any, maxFieldsToConsume int, onlyBufWrite bool, writeOffset int) (bytesWritten, fieldsConsumed int) {
if len(fields) == 0 {
return
}
if inputStruct == nil {
logging.ErrLog.Panicf("UniformBuffer.SetStruct called with a value that is nil")
}
structVal := reflect.ValueOf(inputStruct)
if structVal.Kind() != reflect.Struct {
logging.ErrLog.Panicf("UniformBuffer.SetStruct called with a value that is not a struct. Val=%v\n", inputStruct)
}
// Needed because fieldIndex can move faster than struct fields in case of struct fields
structFieldIndex := 0
for fieldIndex := 0; fieldIndex < len(fields) && fieldIndex < maxFieldsToConsume; fieldIndex++ {
ubField := &fields[fieldIndex]
valField := structVal.Field(structFieldIndex)
fieldsConsumed++
structFieldIndex++
kind := valField.Kind()
if kind == reflect.Pointer {
valField = valField.Elem()
}
var elementType reflect.Type
isArray := kind == reflect.Slice || kind == reflect.Array
if isArray {
elementType = valField.Type().Elem()
kind = elementType.Kind()
} else {
elementType = valField.Type()
}
if isArray {
assert.T(valField.Len() == int(ubField.Count), "ubo field of id=%d is an array/slice field of length=%d but got input of length=%d\n", ubField.Id, ubField.Count, valField.Len())
}
typeMatches := false
bytesWritten = int(ubField.AlignedOffset) + writeOffset
switch ubField.Type {
case DataTypeUint32:
typeMatches = elementType.Name() == "uint32"
if typeMatches {
if isArray {
Write32BitIntegerSliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]uint32))
} else {
Write32BitIntegerToByteBuf(buf, &bytesWritten, uint32(valField.Uint()))
}
}
case DataTypeFloat32:
typeMatches = elementType.Name() == "float32"
if typeMatches {
if isArray {
WriteF32SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]float32))
} else {
WriteF32ToByteBuf(buf, &bytesWritten, float32(valField.Float()))
}
}
case DataTypeInt32:
typeMatches = elementType.Name() == "int32"
if typeMatches {
if isArray {
Write32BitIntegerSliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]int32))
} else {
Write32BitIntegerToByteBuf(buf, &bytesWritten, uint32(valField.Int()))
}
}
case DataTypeVec2:
typeMatches = elementType.Name() == "Vec2"
if typeMatches {
if isArray {
WriteVec2SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec2))
} else {
v2 := valField.Interface().(gglm.Vec2)
WriteF32SliceToByteBuf(buf, &bytesWritten, v2.Data[:])
}
}
case DataTypeVec3:
typeMatches = elementType.Name() == "Vec3"
if typeMatches {
if isArray {
WriteVec3SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec3))
} else {
v3 := valField.Interface().(gglm.Vec3)
WriteF32SliceToByteBuf(buf, &bytesWritten, v3.Data[:])
}
}
case DataTypeVec4:
typeMatches = elementType.Name() == "Vec4"
if typeMatches {
if isArray {
WriteVec4SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec4))
} else {
v3 := valField.Interface().(gglm.Vec4)
WriteF32SliceToByteBuf(buf, &bytesWritten, v3.Data[:])
}
}
case DataTypeMat2:
typeMatches = elementType.Name() == "Mat2"
if typeMatches {
if isArray {
m2Arr := valField.Interface().([]gglm.Mat2)
WriteMat2SliceToByteBufWithAlignment(buf, &bytesWritten, 16*2, m2Arr)
} else {
m := valField.Interface().(gglm.Mat2)
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
}
}
case DataTypeMat3:
typeMatches = elementType.Name() == "Mat3"
if typeMatches {
if isArray {
m3Arr := valField.Interface().([]gglm.Mat3)
WriteMat3SliceToByteBufWithAlignment(buf, &bytesWritten, 16*3, m3Arr)
} else {
m := valField.Interface().(gglm.Mat3)
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[2][:])
}
}
case DataTypeMat4:
typeMatches = elementType.Name() == "Mat4"
if typeMatches {
if isArray {
m4Arr := valField.Interface().([]gglm.Mat4)
WriteMat4SliceToByteBufWithAlignment(buf, &bytesWritten, 16*4, m4Arr)
} else {
m := valField.Interface().(gglm.Mat4)
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[2][:])
WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[3][:])
}
}
case DataTypeStruct:
typeMatches = kind == reflect.Struct
if typeMatches {
if isArray {
offset := 0
arrSize := valField.Len()
fieldsToUse := fields[fieldIndex+1:]
for i := 0; i < arrSize; i++ {
setStructBytesWritten, setStructFieldsConsumed := setStruct(fieldsToUse, buf, valField.Index(i).Interface(), elementType.NumField(), true, offset*i)
if offset == 0 {
offset = setStructBytesWritten
padTo16Boundary(&offset)
bytesWritten += offset * arrSize
// Tracking consumed fields is needed because if we have a struct inside another struct
// elementType.NumField() will only give us the fields consumed by the first struct,
// but we need to count all fields of all nested structs inside this one
fieldIndex += setStructFieldsConsumed
fieldsConsumed += setStructFieldsConsumed
}
}
} else {
setStructBytesWritten, setStructFieldsConsumed := setStruct(fields[fieldIndex+1:], buf, valField.Interface(), valField.NumField(), true, writeOffset)
bytesWritten += setStructBytesWritten
fieldIndex += setStructFieldsConsumed
fieldsConsumed += setStructFieldsConsumed
}
}
default:
assert.T(false, "Unknown uniform buffer data type passed. DataType '%d'", ubField.Type)
}
if !typeMatches {
logging.ErrLog.Panicf("Struct field ordering and types must match uniform buffer fields, but at field index %d got UniformBufferField=%v but a struct field of type %s\n", fieldIndex, ubField, valField.String())
}
}
if bytesWritten == 0 {
return 0, fieldsConsumed
}
if !onlyBufWrite {
gl.BufferSubData(gl.UNIFORM_BUFFER, 0, bytesWritten, gl.Ptr(&buf[0]))
}
return bytesWritten - int(fields[0].AlignedOffset) - writeOffset, fieldsConsumed
}
func Write32BitIntegerToByteBuf[T uint32 | int32](buf []byte, startIndex *int, val T) {
assert.T(*startIndex+4 <= len(buf), "failed to write uint32/int32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d", *startIndex, len(buf))
buf[*startIndex] = byte(val)
buf[*startIndex+1] = byte(val >> 8)
buf[*startIndex+2] = byte(val >> 16)
buf[*startIndex+3] = byte(val >> 24)
*startIndex += 4
}
func Write32BitIntegerSliceToByteBufWithAlignment[T uint32 | int32](buf []byte, startIndex *int, alignmentPerField int, vals []T) {
assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write uint32/int32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField)
for i := 0; i < len(vals); i++ {
val := vals[i]
buf[*startIndex] = byte(val)
buf[*startIndex+1] = byte(val >> 8)
buf[*startIndex+2] = byte(val >> 16)
buf[*startIndex+3] = byte(val >> 24)
*startIndex += alignmentPerField
}
}
func WriteF32ToByteBuf(buf []byte, startIndex *int, val float32) {
assert.T(*startIndex+4 <= len(buf), "failed to write float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d", *startIndex, len(buf))
bits := math.Float32bits(val)
buf[*startIndex] = byte(bits)
buf[*startIndex+1] = byte(bits >> 8)
buf[*startIndex+2] = byte(bits >> 16)
buf[*startIndex+3] = byte(bits >> 24)
*startIndex += 4
}
func WriteF32SliceToByteBuf(buf []byte, startIndex *int, vals []float32) {
assert.T(*startIndex+len(vals)*4 <= len(buf), "failed to write slice of float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", *startIndex, len(buf), len(vals)*4)
for i := 0; i < len(vals); i++ {
bits := math.Float32bits(vals[i])
buf[*startIndex] = byte(bits)
buf[*startIndex+1] = byte(bits >> 8)
buf[*startIndex+2] = byte(bits >> 16)
buf[*startIndex+3] = byte(bits >> 24)
*startIndex += 4
}
}
func WriteF32SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerField int, vals []float32) {
assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write slice of float32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField)
for i := 0; i < len(vals); i++ {
bits := math.Float32bits(vals[i])
buf[*startIndex] = byte(bits)
buf[*startIndex+1] = byte(bits >> 8)
buf[*startIndex+2] = byte(bits >> 16)
buf[*startIndex+3] = byte(bits >> 24)
*startIndex += alignmentPerField
}
}
func WriteVec2SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec2) {
assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)
for i := 0; i < len(vals); i++ {
bitsX := math.Float32bits(vals[i].X())
bitsY := math.Float32bits(vals[i].Y())
buf[*startIndex] = byte(bitsX)
buf[*startIndex+1] = byte(bitsX >> 8)
buf[*startIndex+2] = byte(bitsX >> 16)
buf[*startIndex+3] = byte(bitsX >> 24)
buf[*startIndex+4] = byte(bitsY)
buf[*startIndex+5] = byte(bitsY >> 8)
buf[*startIndex+6] = byte(bitsY >> 16)
buf[*startIndex+7] = byte(bitsY >> 24)
*startIndex += alignmentPerVector
}
}
func WriteVec3SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec3) {
assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec3 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)
for i := 0; i < len(vals); i++ {
bitsX := math.Float32bits(vals[i].X())
bitsY := math.Float32bits(vals[i].Y())
bitsZ := math.Float32bits(vals[i].Z())
buf[*startIndex] = byte(bitsX)
buf[*startIndex+1] = byte(bitsX >> 8)
buf[*startIndex+2] = byte(bitsX >> 16)
buf[*startIndex+3] = byte(bitsX >> 24)
buf[*startIndex+4] = byte(bitsY)
buf[*startIndex+5] = byte(bitsY >> 8)
buf[*startIndex+6] = byte(bitsY >> 16)
buf[*startIndex+7] = byte(bitsY >> 24)
buf[*startIndex+8] = byte(bitsZ)
buf[*startIndex+9] = byte(bitsZ >> 8)
buf[*startIndex+10] = byte(bitsZ >> 16)
buf[*startIndex+11] = byte(bitsZ >> 24)
*startIndex += alignmentPerVector
}
}
func WriteVec4SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec4) {
assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec4 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)
for i := 0; i < len(vals); i++ {
bitsX := math.Float32bits(vals[i].X())
bitsY := math.Float32bits(vals[i].Y())
bitsZ := math.Float32bits(vals[i].Z())
bitsW := math.Float32bits(vals[i].W())
buf[*startIndex] = byte(bitsX)
buf[*startIndex+1] = byte(bitsX >> 8)
buf[*startIndex+2] = byte(bitsX >> 16)
buf[*startIndex+3] = byte(bitsX >> 24)
buf[*startIndex+4] = byte(bitsY)
buf[*startIndex+5] = byte(bitsY >> 8)
buf[*startIndex+6] = byte(bitsY >> 16)
buf[*startIndex+7] = byte(bitsY >> 24)
buf[*startIndex+8] = byte(bitsZ)
buf[*startIndex+9] = byte(bitsZ >> 8)
buf[*startIndex+10] = byte(bitsZ >> 16)
buf[*startIndex+11] = byte(bitsZ >> 24)
buf[*startIndex+12] = byte(bitsW)
buf[*startIndex+13] = byte(bitsW >> 8)
buf[*startIndex+14] = byte(bitsW >> 16)
buf[*startIndex+15] = byte(bitsW >> 24)
*startIndex += alignmentPerVector
}
}
func WriteMat2SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat2) {
assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)
for i := 0; i < len(vals); i++ {
m := &vals[i]
WriteVec2SliceToByteBufWithAlignment(
buf,
startIndex,
16,
[]gglm.Vec2{
{Data: m.Data[0]},
{Data: m.Data[1]},
},
)
}
}
func WriteMat3SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat3) {
assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat3 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)
for i := 0; i < len(vals); i++ {
m := &vals[i]
WriteVec3SliceToByteBufWithAlignment(
buf,
startIndex,
16,
[]gglm.Vec3{
{Data: m.Data[0]},
{Data: m.Data[1]},
{Data: m.Data[2]},
},
)
}
}
func WriteMat4SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat4) {
assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)
for i := 0; i < len(vals); i++ {
m := &vals[i]
WriteVec4SliceToByteBufWithAlignment(
buf,
startIndex,
16,
[]gglm.Vec4{
{Data: m.Data[0]},
{Data: m.Data[1]},
{Data: m.Data[2]},
{Data: m.Data[3]},
},
)
}
}
func ReflectValueMatchesUniformBufferField(v reflect.Value, ubField *UniformBufferField) bool {
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
switch ubField.Type {
case DataTypeUint32:
t := v.Type()
return t.Name() == "uint32"
case DataTypeFloat32:
t := v.Type()
return t.Name() == "float32"
case DataTypeInt32:
t := v.Type()
return t.Name() == "int32"
case DataTypeVec2:
_, ok := v.Interface().(gglm.Vec2)
return ok
case DataTypeVec3:
_, ok := v.Interface().(gglm.Vec3)
return ok
case DataTypeVec4:
_, ok := v.Interface().(gglm.Vec4)
return ok
case DataTypeMat2:
_, ok := v.Interface().(gglm.Mat2)
return ok
case DataTypeMat3:
_, ok := v.Interface().(gglm.Mat3)
return ok
case DataTypeMat4:
_, ok := v.Interface().(gglm.Mat4)
return ok
default:
assert.T(false, "Unknown uniform buffer data type passed. DataType '%d'", ubField.Type)
return false
}
}
func NewUniformBuffer(fields []UniformBufferFieldInput, usage BufUsage) UniformBuffer {
ub := UniformBuffer{}
ub.Size = addUniformBufferFieldsToArray(0, &ub.Fields, fields)
gl.GenBuffers(1, &ub.Id)
if ub.Id == 0 {
logging.ErrLog.Panicln("Failed to create OpenGL buffer for a uniform buffer")
}
ub.Bind()
gl.BufferData(gl.UNIFORM_BUFFER, int(ub.Size), gl.Ptr(nil), usage.ToGL())
ub.UnBind()
return ub
}

View File

@ -55,7 +55,7 @@ func NewVertexBuffer(layout ...Element) VertexBuffer {
gl.GenBuffers(1, &vb.Id)
if vb.Id == 0 {
logging.ErrLog.Println("Failed to create OpenGL buffer")
logging.ErrLog.Panicln("Failed to create OpenGL buffer")
}
vb.SetLayout(layout...)

View File

@ -9,7 +9,6 @@ import (
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/assets"
"github.com/bloeys/nmage/input"
"github.com/bloeys/nmage/renderer"
"github.com/bloeys/nmage/timing"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/go-gl/gl/v4.1-core/gl"
@ -31,7 +30,6 @@ type Window struct {
SDLWin *sdl.Window
GlCtx sdl.GLContext
EventCallbacks []func(sdl.Event)
Rend renderer.Render
}
func (w *Window) handleInputs() {
@ -109,9 +107,9 @@ func (w *Window) handleInputs() {
}
// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
imIo.SetMouseButtonDown(imgui.MouseButtonLeft, isSdlButtonLeftDown)
imIo.SetMouseButtonDown(imgui.MouseButtonRight, isSdlButtonRightDown)
imIo.SetMouseButtonDown(imgui.MouseButtonMiddle, isSdlButtonMiddleDown)
imIo.SetMouseButtonDown(int(imgui.MouseButtonLeft), isSdlButtonLeftDown)
imIo.SetMouseButtonDown(int(imgui.MouseButtonRight), isSdlButtonRightDown)
imIo.SetMouseButtonDown(int(imgui.MouseButtonMiddle), isSdlButtonMiddleDown)
}
func (w *Window) handleWindowResize() {
@ -170,22 +168,21 @@ func initSDL() error {
return nil
}
func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (Window, error) {
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags, rend)
func CreateOpenGLWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) {
return createWindow(title, x, y, width, height, WindowFlags_OPENGL|flags)
}
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags, rend renderer.Render) (Window, error) {
return createWindow(title, sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, width, height, WindowFlags_OPENGL|flags, rend)
func CreateOpenGLWindowCentered(title string, width, height int32, flags WindowFlags) (Window, error) {
return createWindow(title, sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, width, height, WindowFlags_OPENGL|flags)
}
func createWindow(title string, x, y, width, height int32, flags WindowFlags, rend renderer.Render) (Window, error) {
func createWindow(title string, x, y, width, height int32, flags WindowFlags) (Window, error) {
assert.T(isInited, "engine.Init() was not called!")
win := Window{
SDLWin: nil,
EventCallbacks: make([]func(sdl.Event), 0),
Rend: rend,
}
var err error

View File

@ -1,6 +1,7 @@
package engine
import (
"github.com/bloeys/nmage/renderer"
"github.com/bloeys/nmage/timing"
nmageimgui "github.com/bloeys/nmage/ui/imgui"
"github.com/go-gl/gl/v4.1-core/gl"
@ -20,7 +21,7 @@ type Game interface {
DeInit()
}
func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
func Run(g Game, w *Window, rend renderer.Render, ui nmageimgui.ImguiInfo) {
isRunning = true
@ -40,7 +41,6 @@ func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
for isRunning {
//PERF: Cache these
width, height = w.SDLWin.GetSize()
fbWidth, fbHeight = w.SDLWin.GLGetDrawableSize()
@ -56,7 +56,7 @@ func Run(g Game, w *Window, ui nmageimgui.ImguiInfo) {
w.SDLWin.GLSwap()
g.FrameEnd()
w.Rend.FrameEnd()
rend.FrameEnd()
timing.FrameEnded()
}

11
go.mod
View File

@ -1,6 +1,6 @@
module github.com/bloeys/nmage
go 1.22
go 1.23
require github.com/veandco/go-sdl2 v0.4.35
@ -8,12 +8,15 @@ require github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6
require (
github.com/bloeys/assimp-go v0.4.4
github.com/bloeys/gglm v0.49.0
github.com/bloeys/gglm v0.50.0
)
require (
github.com/AllenDang/cimgui-go v0.0.0-20230720025235-f2ff398a66b2
github.com/AllenDang/cimgui-go v0.0.0-20240912193335-545751598105
github.com/mandykoh/prism v0.35.1
)
require github.com/mandykoh/go-parallel v0.1.0 // indirect
require (
github.com/mandykoh/go-parallel v0.1.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
)

8
go.sum
View File

@ -1,9 +1,11 @@
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/AllenDang/cimgui-go v0.0.0-20240912193335-545751598105 h1:bhXqv2EG5YCLdgkLSFCpqeVz4cCoNbi4RDFrHrwxQ1o=
github.com/AllenDang/cimgui-go v0.0.0-20240912193335-545751598105/go.mod h1:CYfBRenCaNtSvKVzChYh6gswUSo6c5IUcYeV6eCCRw0=
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.49.0 h1:YtbyHpszYhjnxw7KVV0LaCdBktRMqfGx/i37EMomxsE=
github.com/bloeys/gglm v0.49.0/go.mod h1:qwJQ0WzV191wAMwlGicbfbChbKoSedMk7gFFX6GnyOk=
github.com/bloeys/gglm v0.50.0 h1:DlGLp9z8KMNx+hNR6PjnPmC0HjDRC19QwAKL1iwhOxs=
github.com/bloeys/gglm v0.50.0/go.mod h1:5s2U/NiOrtJyrSup1j8wK+QOBmGIO03ub0LHMvuNSK8=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/mandykoh/go-parallel v0.1.0 h1:7vJMNMC4dsbgZdkAb2A8tV5ENY1v7VxIO1wzQWZoT8k=
@ -15,6 +17,8 @@ github.com/veandco/go-sdl2 v0.4.35/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofe
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=

628
main.go

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
package materials
import (
_ "unsafe"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/assert"
"github.com/bloeys/nmage/assets"
@ -9,6 +11,16 @@ import (
"github.com/go-gl/gl/v4.1-core/gl"
)
// @TODO: This noescape magic is to avoid heap allocations done when
// passing vectors or matrices into cgo via set uniform calls.
//
// But I would rather this kind of stuff is done on the gl wrapper level.
// Should we wrap the OpenGL APIs we use ourself?
var (
lastMatId uint32
)
type TextureSlot uint32
const (
@ -43,6 +55,7 @@ func (ms *MaterialSettings) Has(flags MaterialSettings) bool {
}
type Material struct {
Id uint32
Name string
ShaderProg shaders.ShaderProgram
Settings MaterialSettings
@ -111,6 +124,21 @@ func (m *Material) UnBind() {
gl.UseProgram(0)
}
func (m *Material) SetUniformBlockBindingPoint(uniformBlockName string, bindPointIndex uint32) {
nullStr := gl.Str(uniformBlockName + "\x00")
index := gl.GetUniformBlockIndex(m.ShaderProg.Id, nullStr)
assert.T(
index != gl.INVALID_INDEX,
"SetUniformBlockBindingPoint for material=%s (matId=%d; shaderId=%d) failed because the uniform block=%s wasn't found",
m.Name,
m.Id,
m.ShaderProg.Id,
uniformBlockName,
)
gl.UniformBlockBinding(m.ShaderProg.Id, index, bindPointIndex)
}
func (m *Material) GetAttribLoc(attribName string) int32 {
loc, ok := m.AttribLocs[attribName]
@ -118,7 +146,8 @@ func (m *Material) GetAttribLoc(attribName string) int32 {
return loc
}
loc = gl.GetAttribLocation(m.ShaderProg.Id, gl.Str(attribName+"\x00"))
name := gl.Str(attribName + "\x00")
loc = gl.GetAttribLocation(m.ShaderProg.Id, name)
assert.T(loc != -1, "Attribute '"+attribName+"' doesn't exist on material "+m.Name)
m.AttribLocs[attribName] = loc
return loc
@ -131,7 +160,8 @@ func (m *Material) GetUnifLoc(uniformName string) int32 {
return loc
}
loc = gl.GetUniformLocation(m.ShaderProg.Id, gl.Str(uniformName+"\x00"))
name := gl.Str(uniformName + "\x00")
loc = gl.GetUniformLocation(m.ShaderProg.Id, name)
assert.T(loc != -1, "Uniform '"+uniformName+"' doesn't exist on material "+m.Name)
m.UnifLocs[uniformName] = loc
return loc
@ -154,33 +184,86 @@ func (m *Material) SetUnifFloat32(uniformName string, val float32) {
}
func (m *Material) SetUnifVec2(uniformName string, vec2 *gglm.Vec2) {
gl.ProgramUniform2fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec2.Data[0])
internalSetUnifVec2(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec2)
}
//go:noescape
//go:linkname internalSetUnifVec2 github.com/bloeys/nmage/materials.SetUnifVec2
func internalSetUnifVec2(shaderProgId uint32, unifLoc int32, vec2 *gglm.Vec2)
func SetUnifVec2(shaderProgId uint32, unifLoc int32, vec2 *gglm.Vec2) {
gl.ProgramUniform2fv(shaderProgId, unifLoc, 1, &vec2.Data[0])
}
func (m *Material) SetUnifVec3(uniformName string, vec3 *gglm.Vec3) {
gl.ProgramUniform3fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec3.Data[0])
internalSetUnifVec3(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec3)
}
//go:noescape
//go:linkname internalSetUnifVec3 github.com/bloeys/nmage/materials.SetUnifVec3
func internalSetUnifVec3(shaderProgId uint32, unifLoc int32, vec3 *gglm.Vec3)
func SetUnifVec3(shaderProgId uint32, unifLoc int32, vec3 *gglm.Vec3) {
gl.ProgramUniform3fv(shaderProgId, unifLoc, 1, &vec3.Data[0])
}
func (m *Material) SetUnifVec4(uniformName string, vec4 *gglm.Vec4) {
gl.ProgramUniform4fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, &vec4.Data[0])
internalSetUnifVec4(m.ShaderProg.Id, m.GetUnifLoc(uniformName), vec4)
}
//go:noescape
//go:linkname internalSetUnifVec4 github.com/bloeys/nmage/materials.SetUnifVec4
func internalSetUnifVec4(shaderProgId uint32, unifLoc int32, vec4 *gglm.Vec4)
func SetUnifVec4(shaderProgId uint32, unifLoc int32, vec4 *gglm.Vec4) {
gl.ProgramUniform4fv(shaderProgId, unifLoc, 1, &vec4.Data[0])
}
func (m *Material) SetUnifMat2(uniformName string, mat2 *gglm.Mat2) {
gl.ProgramUniformMatrix2fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat2.Data[0][0])
internalSetUnifMat2(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat2)
}
//go:noescape
//go:linkname internalSetUnifMat2 github.com/bloeys/nmage/materials.SetUnifMat2
func internalSetUnifMat2(shaderProgId uint32, unifLoc int32, mat2 *gglm.Mat2)
func SetUnifMat2(shaderProgId uint32, unifLoc int32, mat2 *gglm.Mat2) {
gl.ProgramUniformMatrix2fv(shaderProgId, unifLoc, 1, false, &mat2.Data[0][0])
}
func (m *Material) SetUnifMat3(uniformName string, mat3 *gglm.Mat3) {
gl.ProgramUniformMatrix3fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat3.Data[0][0])
internalSetUnifMat3(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat3)
}
//go:noescape
//go:linkname internalSetUnifMat3 github.com/bloeys/nmage/materials.SetUnifMat3
func internalSetUnifMat3(shaderProgId uint32, unifLoc int32, mat3 *gglm.Mat3)
func SetUnifMat3(shaderProgId uint32, unifLoc int32, mat3 *gglm.Mat3) {
gl.ProgramUniformMatrix3fv(shaderProgId, unifLoc, 1, false, &mat3.Data[0][0])
}
func (m *Material) SetUnifMat4(uniformName string, mat4 *gglm.Mat4) {
gl.ProgramUniformMatrix4fv(m.ShaderProg.Id, m.GetUnifLoc(uniformName), 1, false, &mat4.Data[0][0])
internalSetUnifMat4(m.ShaderProg.Id, m.GetUnifLoc(uniformName), mat4)
}
//go:noescape
//go:linkname internalSetUnifMat4 github.com/bloeys/nmage/materials.SetUnifMat4
func internalSetUnifMat4(shaderProgId uint32, unifLoc int32, mat4 *gglm.Mat4)
func SetUnifMat4(shaderProgId uint32, unifLoc int32, mat4 *gglm.Mat4) {
gl.ProgramUniformMatrix4fv(shaderProgId, unifLoc, 1, false, &mat4.Data[0][0])
}
func (m *Material) Delete() {
gl.DeleteProgram(m.ShaderProg.Id)
}
func getNewMatId() uint32 {
lastMatId++
return lastMatId
}
func NewMaterial(matName, shaderPath string) Material {
shdrProg, err := shaders.LoadAndCompileCombinedShader(shaderPath)
@ -189,6 +272,7 @@ func NewMaterial(matName, shaderPath string) Material {
}
return Material{
Id: getNewMatId(),
Name: matName,
ShaderProg: shdrProg,
UnifLocs: make(map[string]int32),
@ -209,6 +293,7 @@ func NewMaterialSrc(matName string, shaderSrc []byte) Material {
}
return Material{
Id: getNewMatId(),
Name: matName,
ShaderProg: shdrProg,
UnifLocs: make(map[string]int32),

View File

@ -151,7 +151,7 @@ func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (Mesh, e
indexBufData = append(indexBufData, indices...)
}
vbo.SetData(vertexBufData, buffers.BufUsage_Static)
vbo.SetData(vertexBufData, buffers.BufUsage_Static_Draw)
ibo.SetData(indexBufData)
mesh.Vao.AddVertexBuffer(vbo)

View File

@ -12,21 +12,21 @@ import (
var _ renderer.Render = &Rend3DGL{}
type Rend3DGL struct {
BoundVao *buffers.VertexArray
BoundMesh *meshes.Mesh
BoundMat *materials.Material
BoundVaoId uint32
BoundMatId uint32
BoundMeshVaoId uint32
}
func (r *Rend3DGL) DrawMesh(mesh *meshes.Mesh, modelMat *gglm.TrMat, mat *materials.Material) {
if mesh != r.BoundMesh {
if mesh.Vao.Id != r.BoundMeshVaoId {
mesh.Vao.Bind()
r.BoundMesh = mesh
r.BoundMeshVaoId = mesh.Vao.Id
}
if mat != r.BoundMat {
if mat.Id != r.BoundMatId {
mat.Bind()
r.BoundMat = mat
r.BoundMatId = mat.Id
}
if mat.Settings.Has(materials.MaterialSettings_HasModelMtx) {
@ -45,14 +45,14 @@ func (r *Rend3DGL) DrawMesh(mesh *meshes.Mesh, modelMat *gglm.TrMat, mat *materi
func (r *Rend3DGL) DrawVertexArray(mat *materials.Material, vao *buffers.VertexArray, firstElement int32, elementCount int32) {
if vao != r.BoundVao {
if vao.Id != r.BoundVaoId {
vao.Bind()
r.BoundVao = vao
r.BoundVaoId = vao.Id
}
if mat != r.BoundMat {
if mat.Id != r.BoundMatId {
mat.Bind()
r.BoundMat = mat
r.BoundMatId = mat.Id
}
gl.DrawArrays(gl.TRIANGLES, firstElement, elementCount)
@ -60,14 +60,14 @@ func (r *Rend3DGL) DrawVertexArray(mat *materials.Material, vao *buffers.VertexA
func (r *Rend3DGL) DrawCubemap(mesh *meshes.Mesh, mat *materials.Material) {
if mesh != r.BoundMesh {
if mesh.Vao.Id != r.BoundMeshVaoId {
mesh.Vao.Bind()
r.BoundMesh = mesh
r.BoundMeshVaoId = mesh.Vao.Id
}
if mat != r.BoundMat {
if mat.Id != r.BoundMatId {
mat.Bind()
r.BoundMat = mat
r.BoundMatId = mat.Id
}
for i := 0; i < len(mesh.SubMeshes); i++ {
@ -76,8 +76,9 @@ func (r *Rend3DGL) DrawCubemap(mesh *meshes.Mesh, mat *materials.Material) {
}
func (r3d *Rend3DGL) FrameEnd() {
r3d.BoundMesh = nil
r3d.BoundMat = nil
r3d.BoundVaoId = 0
r3d.BoundMatId = 0
r3d.BoundMeshVaoId = 0
}
func NewRend3DGL() *Rend3DGL {

View File

@ -14,33 +14,25 @@ layout(location=3) in vec2 vertUV0In;
layout(location=4) in vec3 vertColorIn;
//
// Uniforms
// UBOs
//
uniform vec3 camPos;
uniform mat4 modelMat;
uniform mat3 normalMat;
uniform mat4 projViewMat;
uniform mat4 dirLightProjViewMat;
uniform mat4 spotLightProjViewMats[NUM_SPOT_LIGHTS];
struct DirLight {
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
sampler2D shadowMap;
};
uniform DirLight dirLight;
uniform sampler2D dirLightShadowMap;
struct PointLight {
vec3 pos;
vec3 diffuseColor;
vec3 specularColor;
float constant;
float linear;
float quadratic;
float radius;
float falloff;
float maxBias;
float nearPlane;
float farPlane;
};
uniform PointLight pointLights[NUM_POINT_LIGHTS];
struct SpotLight {
vec3 pos;
@ -50,7 +42,25 @@ struct SpotLight {
float innerCutoff;
float outerCutoff;
};
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
layout (std140) uniform GlobalMatrices {
vec3 camPos;
mat4 projViewMat;
};
layout (std140) uniform Lights {
DirLight dirLight;
PointLight pointLights[NUM_POINT_LIGHTS];
SpotLight spotLights[NUM_SPOT_LIGHTS];
vec3 ambientColor;
};
//
// Uniforms
//
uniform mat4 modelMat;
uniform mat4 dirLightProjViewMat;
uniform mat4 spotLightProjViewMats[NUM_SPOT_LIGHTS];
//
// Outputs
@ -154,20 +164,19 @@ struct DirLight {
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
sampler2D shadowMap;
};
uniform DirLight dirLight;
uniform sampler2D dirLightShadowMap;
struct PointLight {
vec3 pos;
vec3 diffuseColor;
vec3 specularColor;
float constant;
float linear;
float quadratic;
float radius;
float falloff;
float maxBias;
float nearPlane;
float farPlane;
};
uniform PointLight pointLights[NUM_POINT_LIGHTS];
uniform samplerCubeArray pointLightCubeShadowMaps;
struct SpotLight {
@ -178,10 +187,19 @@ struct SpotLight {
float innerCutoff;
float outerCutoff;
};
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
uniform sampler2DArray spotLightShadowMaps;
uniform vec3 ambientColor = vec3(0.2, 0.2, 0.2);
layout (std140) uniform GlobalMatrices {
vec3 camPos;
mat4 projViewMat;
};
layout (std140) uniform Lights {
DirLight dirLight;
PointLight pointLights[NUM_POINT_LIGHTS];
SpotLight spotLights[NUM_SPOT_LIGHTS];
vec3 ambientColor;
};
//
// Outputs
@ -248,34 +266,88 @@ vec3 CalcDirLight()
vec3 finalSpecular = specularAmount * dirLight.specularColor * specularTexColor.rgb;
// Shadow
float shadow = CalcDirShadow(dirLight.shadowMap, lightDir);
float shadow = CalcDirShadow(dirLightShadowMap, lightDir);
return (finalDiffuse + finalSpecular) * (1 - shadow);
}
float CalcPointShadow(int lightIndex, vec3 worldLightPos, vec3 tangentLightDir, float farPlane) {
float CalcPointShadow(int lightIndex, vec3 worldLightPos, vec3 tangentLightDir, float maxBias, float nearPlane, float farPlane) {
vec3 lightToFrag = fragPos - worldLightPos;
// Get depth of current fragment
float currentDepth = length(lightToFrag);
if (currentDepth < nearPlane) {
return 0;
}
float closestDepth = texture(pointLightCubeShadowMaps, vec4(lightToFrag, lightIndex)).r;
// We stored depth in the cubemap in the range [0, 1], so now we move back to [0, farPlane]
closestDepth *= farPlane;
// Get depth of current fragment
float currentDepth = length(lightToFrag);
float bias = max(maxBias * (1 - dot(normalizedVertNorm, tangentLightDir)), 0.005);
float bias = max(0.05 * (1 - dot(normalizedVertNorm, tangentLightDir)), 0.005);
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
float shadow = currentDepth - bias > closestDepth ? 1 : 0;
return shadow;
}
//
// The following point light attenuation formulas
// are from https://lisyarus.github.io/blog/posts/point-light-attenuation.html
//
// I found them more intuitive than the standard implementation and it also ensures
// we have zero light at the selected distance.
//
float sqr(float x)
{
return x * x;
}
// This version doesn't have a harsh cutoff at radius
float AttenuateNoCusp(float dist, float radius, float falloff)
{
// Since we only use this as attenuation and max intensity defines
// the max output value, anything more than 1 would increase
// the output of the light, which I don't think makes sense for
// our attenuation purposes.
//
// Seems to me this can be done simply by increasing color values above 255.
//
// Forcing to 1 for now.
#define MAX_INTENSITY 1
float s = dist / radius;
if (s >= 1.0)
return 0.0;
float s2 = sqr(s);
return MAX_INTENSITY * sqr(1 - s2) / (1 + falloff * s2);
}
// This version has a harsh/immediate cutoff at radius
float AttenuateCusp(float dist, float radius, float falloff)
{
#define MAX_INTENSITY 1
float s = dist / radius;
if (s >= 1.0)
return 0.0;
float s2 = sqr(s);
return MAX_INTENSITY * sqr(1 - s2) / (1 + falloff * s);
}
vec3 CalcPointLight(PointLight pointLight, int lightIndex)
{
// Ignore unset lights
if (pointLight.constant == 0){
// Ignore inactive lights
if (pointLight.radius == 0){
return vec3(0);
}
@ -293,10 +365,10 @@ vec3 CalcPointLight(PointLight pointLight, int lightIndex)
// Attenuation
float distToLight = length(tangentLightPos - tangentFragPos);
float attenuation = 1 / (pointLight.constant + pointLight.linear * distToLight + pointLight.quadratic * (distToLight * distToLight));
float attenuation = AttenuateNoCusp(distToLight, pointLight.radius, pointLight.falloff);
// Shadow
float shadow = CalcPointShadow(lightIndex, pointLight.pos, tangentLightDir, pointLight.farPlane);
float shadow = CalcPointShadow(lightIndex, pointLight.pos, tangentLightDir, pointLight.maxBias, pointLight.nearPlane, pointLight.farPlane);
return (finalDiffuse + finalSpecular) * attenuation * (1 - shadow);
}
@ -343,7 +415,10 @@ float CalcSpotShadow(vec3 tangentLightDir, int lightIndex)
vec3 CalcSpotLight(SpotLight light, int lightIndex)
{
if (light.innerCutoff == 0)
// The inner/outer cutoffs are cosine values,
// which means a value of 1 is mainly produced when the input
// is 0 degrees or radians. cos(180) will also be 1, but that's too much :)
if (light.innerCutoff == 1)
return vec3(0);
vec3 tangentLightDir = tangentSpotLightDirections[lightIndex];

View File

@ -1,6 +1,17 @@
package nmageimgui
import (
"unsafe"
// The following is included just so we can get
// the c imports and cgo configs defined here: https://github.com/AllenDang/cimgui-go/blob/main/sdlbackend/sdl_backend.go
//
// This is needed because AllenDang/cimgui-go links sdl2 statically, which requires us
// to explicitly define all the libs it needs, which isn't normally needed if we were dynamically linking.
//
// Without this, we get compilation errors as sdl2 can't find libs it relies on.
_ "github.com/AllenDang/cimgui-go/sdlbackend"
imgui "github.com/AllenDang/cimgui-go"
"github.com/bloeys/gglm/gglm"
"github.com/bloeys/nmage/materials"
@ -129,7 +140,7 @@ func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *im
fontConfigToUse := imgui.NewFontConfig()
if fontConfig != nil {
fontConfigToUse = *fontConfig
fontConfigToUse = fontConfig
}
glyphRangesToUse := imgui.NewGlyphRange()
@ -141,13 +152,13 @@ func (i *ImguiInfo) AddFontTTF(fontPath string, fontSize float32, fontConfig *im
a := imIO.Fonts()
f := a.AddFontFromFileTTFV(fontPath, fontSize, fontConfigToUse, glyphRangesToUse.Data())
pixels, width, height, _ := a.GetTextureDataAsAlpha8()
pixels, width, height, _ := a.TextureDataAsAlpha8()
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, *i.TexID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(width), int32(height), 0, gl.RED, gl.UNSIGNED_BYTE, pixels)
return f
return *f
}
const DefaultImguiShader = `
@ -212,7 +223,7 @@ func NewImGui(shaderPath string) ImguiInfo {
}
imguiInfo := ImguiInfo{
ImCtx: imgui.CreateContext(),
ImCtx: *imgui.CreateContext(),
Mat: imguiMat,
TexID: new(uint32),
}
@ -233,11 +244,12 @@ func NewImGui(shaderPath string) ImguiInfo {
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
pixels, width, height, _ := io.Fonts().GetTextureDataAsAlpha8()
pixels, width, height, _ := io.Fonts().TextureDataAsAlpha8()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(width), int32(height), 0, gl.RED, gl.UNSIGNED_BYTE, pixels)
// Store our identifier
io.Fonts().SetTexID(imgui.TextureID(imguiInfo.TexID))
io.Fonts().SetTexID(imgui.TextureID{Data: uintptr(unsafe.Pointer(imguiInfo.TexID))})
//Shader attributes
imguiInfo.Mat.Bind()