Compare commits

...

7 Commits

Author SHA1 Message Date
524ef068f0 Add comment 2024-04-12 23:57:24 +04:00
b060dcdbe9 Go 1.22+fix input bug 2024-04-12 23:55:21 +04:00
e22525e2ee Default to srgba textures 2024-04-12 23:38:51 +04:00
ee61373069 Blinn-phong 2024-04-12 23:28:59 +04:00
1f922b6a47 Enable stencil test 2024-04-12 23:02:27 +04:00
9d7bdc0196 Improve error messages 2024-04-12 22:40:08 +04:00
83922f1908 Spot lights 2024-04-12 21:09:14 +04:00
8 changed files with 230 additions and 56 deletions

View File

@ -12,7 +12,7 @@ jobs:
- name: Install golang
uses: actions/setup-go@v3
with:
go-version: '>=1.18'
go-version: '>=1.22'
- name: Install assimp-go dylib
run: sudo mkdir -p /usr/local/lib && sudo wget https://github.com/bloeys/assimp-go/releases/download/v0.4.2/libassimp_darwin_amd64.dylib -O /usr/local/lib/libassimp.5.dylib

View File

@ -45,7 +45,7 @@ type TextureLoadOptions struct {
WriteToCache bool
GenMipMaps bool
KeepPixelsInMem bool
TextureIsSrgba bool
NoSrgba bool
}
type Cubemap struct {
@ -103,9 +103,9 @@ func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, erro
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.RGBA8)
if loadOptions.TextureIsSrgba {
internalFormat = gl.SRGB_ALPHA
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
@ -151,9 +151,9 @@ func LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (T
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.RGBA8)
if loadOptions.TextureIsSrgba {
internalFormat = gl.SRGB_ALPHA
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
@ -216,9 +216,9 @@ func LoadTextureJpeg(file string, loadOptions *TextureLoadOptions) (Texture, err
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.RGBA8)
if loadOptions.TextureIsSrgba {
internalFormat = gl.SRGB_ALPHA
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
@ -288,9 +288,9 @@ func LoadCubemapTextures(rightTex, leftTex, topTex, botTex, frontTex, backTex st
height := int32(nrgbaImg.Bounds().Dy())
width := int32(nrgbaImg.Bounds().Dx())
internalFormat := int32(gl.RGBA8)
if loadOptions.TextureIsSrgba {
internalFormat = gl.SRGB_ALPHA
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X)+i, 0, internalFormat, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&nrgbaImg.Pix[0]))

View File

@ -39,6 +39,20 @@ func (w *Window) handleInputs() {
imguiCaptureMouse := imIo.WantCaptureMouse()
imguiCaptureKeyboard := imIo.WantCaptureKeyboard()
// These two are to fix a bug where state isn't cleared
// even after imgui captures the keyboard/mouse.
//
// For example, if player is moving due to key held and then imgui captures the keyboard,
// the player keeps moving even when the key is no longer pressed because the input system never
// receives the key up event.
if imguiCaptureMouse {
input.ClearMouseState()
}
if imguiCaptureKeyboard {
input.ClearKeyboardState()
}
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
//Fire callbacks
@ -229,6 +243,7 @@ func initOpenGL() error {
}
gl.Enable(gl.DEPTH_TEST)
gl.Enable(gl.STENCIL_TEST)
gl.Enable(gl.CULL_FACE)
gl.CullFace(gl.BACK)
gl.FrontFace(gl.CCW)
@ -252,7 +267,6 @@ func SetSrgbFramebuffer(isEnabled bool) {
}
func SetVSync(enabled bool) {
assert.T(isInited, "engine.Init was not called!")
if enabled {
sdl.GLSetSwapInterval(1)

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/bloeys/nmage
go 1.18
go 1.22
require github.com/veandco/go-sdl2 v0.4.35

View File

@ -40,15 +40,17 @@ var (
func EventLoopStart() {
for _, v := range keyMap {
for k, v := range keyMap {
v.IsPressedThisFrame = false
v.IsReleasedThisFrame = false
keyMap[k] = v
}
for _, v := range mouseBtnMap {
for k, v := range mouseBtnMap {
v.IsPressedThisFrame = false
v.IsReleasedThisFrame = false
v.IsDoubleClicked = false
mouseBtnMap[k] = v
}
mouseMotion.XDelta = 0
@ -60,6 +62,16 @@ func EventLoopStart() {
quitRequested = false
}
func ClearKeyboardState() {
clear(keyMap)
}
func ClearMouseState() {
clear(mouseBtnMap)
mouseMotion = mouseMotionState{}
mouseWheel = mouseWheelState{}
}
func HandleQuitEvent(e *sdl.QuitEvent) {
quitRequested = true
}

158
main.go
View File

@ -26,12 +26,14 @@ import (
/*
@TODO:
- Rendering:
- Phong lighting model
- Point lights
- Spotlights
- Blinn-Phong lighting model
- Directional lights
- Point lights
- Spotlights ✅
- HDR
- Cascaded shadow mapping
- Skeletal animations
- Proper model loading (i.e. load model by reading all its meshes, textures, and so on together)
- Create VAO struct independent from VBO to support multi-VBO use cases (e.g. instancing)
- Renderer batching
- Scene graph
@ -62,6 +64,7 @@ type PointLight struct {
}
type SpotLight struct {
Pos gglm.Vec3
Dir gglm.Vec3
DiffuseColor gglm.Vec3
SpecularColor gglm.Vec3
@ -69,6 +72,18 @@ type SpotLight struct {
OuterCutoff float32
}
// SetCutoffs properly sets the cosine values of the cutoffs using the passed
// degrees.
//
// The light has full intensity within the inner cutoff, falloff between
// inner-outer cutoff, and zero light beyond the outer cutoff.
//
// The inner cuttoff degree must be *smaller* than the outer cutoff
func (s *SpotLight) SetCutoffs(innerCutoffAngleDeg, outerCutoffAngleDeg float32) {
s.InnerCutoff = gglm.Cos32(innerCutoffAngleDeg * gglm.Deg2Rad)
s.OuterCutoff = gglm.Cos32(outerCutoffAngleDeg * gglm.Deg2Rad)
}
const (
camSpeed = 15
mouseSensitivity = 0.5
@ -148,6 +163,17 @@ var (
Quadratic: 0.032,
},
}
spotLights = [...]SpotLight{
{
Pos: *gglm.NewVec3(0, 5, 0),
Dir: *gglm.NewVec3(0, -1, 0),
DiffuseColor: *gglm.NewVec3(0, 1, 1),
SpecularColor: *gglm.NewVec3(1, 1, 1),
// These must be cosine values
InnerCutoff: gglm.Cos32(15 * gglm.Deg2Rad),
OuterCutoff: gglm.Cos32(20 * gglm.Deg2Rad),
},
}
)
type Game struct {
@ -324,27 +350,27 @@ func (g *Game) Init() {
}
//Load textures
whiteTex, err := assets.LoadTexturePNG("./res/textures/white.png", &assets.TextureLoadOptions{TextureIsSrgba: true})
whiteTex, err := assets.LoadTexturePNG("./res/textures/white.png", &assets.TextureLoadOptions{})
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
blackTex, err := assets.LoadTexturePNG("./res/textures/black.png", &assets.TextureLoadOptions{TextureIsSrgba: true})
blackTex, err := assets.LoadTexturePNG("./res/textures/black.png", &assets.TextureLoadOptions{})
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
containerDiffuseTex, err := assets.LoadTexturePNG("./res/textures/container-diffuse.png", &assets.TextureLoadOptions{TextureIsSrgba: true})
containerDiffuseTex, err := assets.LoadTexturePNG("./res/textures/container-diffuse.png", &assets.TextureLoadOptions{})
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
containerSpecularTex, err := assets.LoadTexturePNG("./res/textures/container-specular.png", &assets.TextureLoadOptions{TextureIsSrgba: true})
containerSpecularTex, err := assets.LoadTexturePNG("./res/textures/container-specular.png", &assets.TextureLoadOptions{})
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
palleteTex, err := assets.LoadTexturePNG("./res/textures/pallete-endesga-64-1x.png", &assets.TextureLoadOptions{TextureIsSrgba: true})
palleteTex, err := assets.LoadTexturePNG("./res/textures/pallete-endesga-64-1x.png", &assets.TextureLoadOptions{})
if err != nil {
logging.ErrLog.Fatalln("Failed to load texture. Err: ", err)
}
@ -353,7 +379,7 @@ func (g *Game) Init() {
"./res/textures/sb-right.jpg", "./res/textures/sb-left.jpg",
"./res/textures/sb-top.jpg", "./res/textures/sb-bottom.jpg",
"./res/textures/sb-front.jpg", "./res/textures/sb-back.jpg",
&assets.TextureLoadOptions{TextureIsSrgba: true},
&assets.TextureLoadOptions{},
)
if err != nil {
logging.ErrLog.Fatalln("Failed to load cubemap. Err: ", err)
@ -456,6 +482,36 @@ func (g *Game) updateLights() {
containerMat.SetUnifFloat32(indexString+".quadratic", pl.Quadratic)
palleteMat.SetUnifFloat32(indexString+".quadratic", pl.Quadratic)
}
for i := 0; i < len(spotLights); i++ {
l := &spotLights[i]
indexString := "spotLights[" + strconv.Itoa(i) + "]"
whiteMat.SetUnifVec3(indexString+".pos", &l.Pos)
containerMat.SetUnifVec3(indexString+".pos", &l.Pos)
palleteMat.SetUnifVec3(indexString+".pos", &l.Pos)
whiteMat.SetUnifVec3(indexString+".dir", &l.Dir)
containerMat.SetUnifVec3(indexString+".dir", &l.Dir)
palleteMat.SetUnifVec3(indexString+".dir", &l.Dir)
whiteMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
containerMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
palleteMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
whiteMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
containerMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
palleteMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
whiteMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
containerMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
palleteMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
whiteMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
containerMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
palleteMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
}
}
func (g *Game) Update() {
@ -544,35 +600,91 @@ func (g *Game) showDebugWindow() {
imgui.Spacing()
// Point lights
imgui.Text("Point Lights")
if imgui.BeginListBoxV("", imgui.Vec2{Y: 200}) {
if imgui.BeginListBoxV("Point Lights", imgui.Vec2{Y: 200}) {
for i := 0; i < len(pointLights); i++ {
pl := &pointLights[i]
indexString := strconv.Itoa(i)
indexNumString := strconv.Itoa(i)
if !imgui.TreeNodeExStrV("Light "+indexString, imgui.TreeNodeFlagsSpanAvailWidth) {
if !imgui.TreeNodeExStrV("Point Light "+indexNumString, imgui.TreeNodeFlagsSpanAvailWidth) {
continue
}
indexString := "pointLights[" + indexNumString + "]"
if imgui.DragFloat3("Pos", &pl.Pos.Data) {
whiteMat.SetUnifVec3("pointLights["+indexString+"].pos", &pl.Pos)
containerMat.SetUnifVec3("pointLights["+indexString+"].pos", &pl.Pos)
palleteMat.SetUnifVec3("pointLights["+indexString+"].pos", &pl.Pos)
whiteMat.SetUnifVec3(indexString+".pos", &pl.Pos)
containerMat.SetUnifVec3(indexString+".pos", &pl.Pos)
palleteMat.SetUnifVec3(indexString+".pos", &pl.Pos)
}
if imgui.DragFloat3("Diffuse Color", &pl.DiffuseColor.Data) {
whiteMat.SetUnifVec3("pointLights["+indexString+"].diffuseColor", &pl.DiffuseColor)
containerMat.SetUnifVec3("pointLights["+indexString+"].diffuseColor", &pl.DiffuseColor)
palleteMat.SetUnifVec3("pointLights["+indexString+"].diffuseColor", &pl.DiffuseColor)
whiteMat.SetUnifVec3(indexString+".diffuseColor", &pl.DiffuseColor)
containerMat.SetUnifVec3(indexString+".diffuseColor", &pl.DiffuseColor)
palleteMat.SetUnifVec3(indexString+".diffuseColor", &pl.DiffuseColor)
}
if imgui.DragFloat3("Specular Color", &pl.SpecularColor.Data) {
whiteMat.SetUnifVec3("pointLights["+indexString+"].specularColor", &pl.SpecularColor)
containerMat.SetUnifVec3("pointLights["+indexString+"].specularColor", &pl.SpecularColor)
palleteMat.SetUnifVec3("pointLights["+indexString+"].specularColor", &pl.SpecularColor)
whiteMat.SetUnifVec3(indexString+".specularColor", &pl.SpecularColor)
containerMat.SetUnifVec3(indexString+".specularColor", &pl.SpecularColor)
palleteMat.SetUnifVec3(indexString+".specularColor", &pl.SpecularColor)
}
imgui.TreePop()
}
imgui.EndListBox()
}
// Spot lights
if imgui.BeginListBoxV("Spot Lights", imgui.Vec2{Y: 200}) {
for i := 0; i < len(spotLights); i++ {
l := &spotLights[i]
indexNumString := strconv.Itoa(i)
if !imgui.TreeNodeExStrV("Spot Light "+indexNumString, imgui.TreeNodeFlagsSpanAvailWidth) {
continue
}
indexString := "spotLights[" + indexNumString + "]"
if imgui.DragFloat3("Pos", &l.Pos.Data) {
whiteMat.SetUnifVec3(indexString+".pos", &l.Pos)
containerMat.SetUnifVec3(indexString+".pos", &l.Pos)
palleteMat.SetUnifVec3(indexString+".pos", &l.Pos)
}
if imgui.DragFloat3("Dir", &l.Dir.Data) {
whiteMat.SetUnifVec3(indexString+".dir", &l.Dir)
containerMat.SetUnifVec3(indexString+".dir", &l.Dir)
palleteMat.SetUnifVec3(indexString+".dir", &l.Dir)
}
if imgui.DragFloat3("Diffuse Color", &l.DiffuseColor.Data) {
whiteMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
containerMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
palleteMat.SetUnifVec3(indexString+".diffuseColor", &l.DiffuseColor)
}
if imgui.DragFloat3("Specular Color", &l.SpecularColor.Data) {
whiteMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
containerMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
palleteMat.SetUnifVec3(indexString+".specularColor", &l.SpecularColor)
}
if imgui.DragFloat("Inner Cutoff", &l.InnerCutoff) {
whiteMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
containerMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
palleteMat.SetUnifFloat32(indexString+".innerCutoff", l.InnerCutoff)
}
if imgui.DragFloat("Outer Cutoff", &l.OuterCutoff) {
whiteMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
containerMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
palleteMat.SetUnifFloat32(indexString+".outerCutoff", l.OuterCutoff)
}
imgui.TreePop()

View File

@ -62,14 +62,11 @@ func NewMesh(name, modelPath string, postProcessFlags asig.PostProcess) (*Mesh,
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))
assert.T(len(firstSubmeshLayout) == len(layoutToUse), "Vertex layout of submesh '%d' of mesh '%s' at path '%s' does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, name, modelPath, firstSubmeshLayout, layoutToUse)
for i := 0; i < len(firstSubmeshLayout); i++ {
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))
}
assert.T(firstSubmeshLayout[i].ElementType == layoutToUse[i].ElementType, "Vertex layout of submesh '%d' of mesh '%s' at path '%s' does not equal vertex layout of the first submesh. Original layout: %v; This layout: %v", i, name, modelPath, firstSubmeshLayout, layoutToUse)
}
}
@ -119,9 +116,9 @@ type arrToInterleave struct {
func (a *arrToInterleave) get(i int) []float32 {
assert.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but both arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V3s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
assert.T(len(a.V2s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
assert.T(len(a.V3s) == 0 || len(a.V4s) == 0, "One array should be set in arrToInterleave, but multiple arrays are set")
if len(a.V2s) > 0 {
return a.V2s[i].Data[:]

View File

@ -64,6 +64,18 @@ struct PointLight {
#define NUM_POINT_LIGHTS 16
uniform PointLight pointLights[NUM_POINT_LIGHTS];
struct SpotLight {
vec3 pos;
vec3 dir;
vec3 diffuseColor;
vec3 specularColor;
float innerCutoff;
float outerCutoff;
};
#define NUM_SPOT_LIGHTS 4
uniform SpotLight spotLights[NUM_SPOT_LIGHTS];
uniform vec3 camPos;
uniform vec3 ambientColor = vec3(0.2, 0.2, 0.2);
@ -90,8 +102,8 @@ vec3 CalcDirLight()
vec3 finalDiffuse = diffuseAmount * dirLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 reflectDir = reflect(-lightDir, normalizedVertNorm);
float specularAmount = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 halfwayDir = normalize(lightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * dirLight.specularColor * specularTexColor.rgb;
return finalDiffuse + finalSpecular;
@ -111,8 +123,8 @@ vec3 CalcPointLight(PointLight pointLight)
vec3 finalDiffuse = diffuseAmount * pointLight.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 reflectDir = reflect(-lightDir, normalizedVertNorm);
float specularAmount = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 halfwayDir = normalize(lightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * pointLight.specularColor * specularTexColor.rgb;
// attenuation
@ -122,9 +134,33 @@ vec3 CalcPointLight(PointLight pointLight)
return (finalDiffuse + finalSpecular) * attenuation;
}
vec3 CalcSpotLight()
vec3 CalcSpotLight(SpotLight light)
{
if (light.innerCutoff == 0)
return vec3(0);
vec3 fragToLightDir = normalize(light.pos - fragPos);
// Spot light cone with full intensity within inner cutoff,
// and falloff between inner-outer cutoffs, and zero
// light after outer cutoff
float theta = dot(fragToLightDir, normalize(-light.dir));
float epsilon = (light.innerCutoff - light.outerCutoff);
float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0);
if (intensity == 0)
return vec3(0);
// Diffuse
float diffuseAmount = max(0.0, dot(normalizedVertNorm, fragToLightDir));
vec3 finalDiffuse = diffuseAmount * light.diffuseColor * diffuseTexColor.rgb;
// Specular
vec3 halfwayDir = normalize(fragToLightDir + viewDir);
float specularAmount = pow(max(dot(normalizedVertNorm, halfwayDir), 0.0), material.shininess);
vec3 finalSpecular = specularAmount * light.specularColor * specularTexColor.rgb;
return (finalDiffuse + finalSpecular) * intensity;
}
void main()
@ -145,7 +181,10 @@ void main()
finalColor += CalcPointLight(pointLights[i]);
}
finalColor += CalcSpotLight();
for (int i = 0; i < NUM_SPOT_LIGHTS; i++)
{
finalColor += CalcSpotLight(spotLights[i]);
}
vec3 finalEmission = emissionTexColor.rgb;
vec3 finalAmbient = ambientColor * diffuseTexColor.rgb;