From ddd8db3cb0742fc278d11aef2936ab9b9a7373df Mon Sep 17 00:00:00 2001 From: bloeys Date: Sat, 13 Apr 2024 08:03:17 +0400 Subject: [PATCH] Framebuffers+unlit shader+screen quad shader --- buffers/framebuffer.go | 295 ++++++++++++++++++++++++++++++++++ main.go | 75 +++++++++ res/shaders/screen-quad.glsl | 45 ++++++ res/shaders/simple-unlit.glsl | 54 +++++++ 4 files changed, 469 insertions(+) create mode 100755 buffers/framebuffer.go create mode 100755 res/shaders/screen-quad.glsl create mode 100755 res/shaders/simple-unlit.glsl diff --git a/buffers/framebuffer.go b/buffers/framebuffer.go new file mode 100755 index 0000000..05ae38e --- /dev/null +++ b/buffers/framebuffer.go @@ -0,0 +1,295 @@ +package buffers + +import ( + "github.com/bloeys/nmage/logging" + "github.com/go-gl/gl/v4.1-core/gl" +) + +type FramebufferAttachmentType int32 + +const ( + FramebufferAttachmentType_Unknown FramebufferAttachmentType = iota + FramebufferAttachmentType_Texture + FramebufferAttachmentType_Renderbuffer +) + +func (f FramebufferAttachmentType) IsValid() bool { + + switch f { + case FramebufferAttachmentType_Texture: + fallthrough + case FramebufferAttachmentType_Renderbuffer: + return true + + default: + return false + } +} + +type FramebufferAttachmentDataFormat int32 + +const ( + FramebufferAttachmentDataFormat_Unknown FramebufferAttachmentDataFormat = iota + FramebufferAttachmentDataFormat_R32Int + FramebufferAttachmentDataFormat_RGBA8 + FramebufferAttachmentDataFormat_SRGBA + FramebufferAttachmentDataFormat_Depth24Stencil8 +) + +func (f FramebufferAttachmentDataFormat) IsColorFormat() bool { + return f == FramebufferAttachmentDataFormat_R32Int || + f == FramebufferAttachmentDataFormat_RGBA8 || + f == FramebufferAttachmentDataFormat_SRGBA +} + +func (f FramebufferAttachmentDataFormat) IsDepthFormat() bool { + return f == FramebufferAttachmentDataFormat_Depth24Stencil8 +} + +func (f FramebufferAttachmentDataFormat) GlInternalFormat() int32 { + + switch f { + case FramebufferAttachmentDataFormat_R32Int: + return gl.R32I + case FramebufferAttachmentDataFormat_RGBA8: + return gl.RGB8 + case FramebufferAttachmentDataFormat_SRGBA: + return gl.SRGB_ALPHA + case FramebufferAttachmentDataFormat_Depth24Stencil8: + return gl.DEPTH24_STENCIL8 + default: + logging.ErrLog.Fatalf("unknown framebuffer attachment data format. Format=%d\n", f) + return 0 + } +} + +func (f FramebufferAttachmentDataFormat) GlFormat() uint32 { + + switch f { + case FramebufferAttachmentDataFormat_R32Int: + return gl.RED_INTEGER + + case FramebufferAttachmentDataFormat_RGBA8: + fallthrough + case FramebufferAttachmentDataFormat_SRGBA: + return gl.RGBA + + case FramebufferAttachmentDataFormat_Depth24Stencil8: + return gl.DEPTH_STENCIL + + default: + logging.ErrLog.Fatalf("unknown framebuffer attachment data format. Format=%d\n", f) + return 0 + } +} + +type FramebufferAttachment struct { + Id uint32 + Type FramebufferAttachmentType + Format FramebufferAttachmentDataFormat +} + +type Framebuffer struct { + Id uint32 + Attachments []FramebufferAttachment + ColorAttachmentsCount uint32 + Width uint32 + Height uint32 +} + +func (fbo *Framebuffer) Bind() { + gl.BindFramebuffer(gl.FRAMEBUFFER, fbo.Id) +} + +func (fbo *Framebuffer) BindWithViewport() { + gl.BindFramebuffer(gl.FRAMEBUFFER, fbo.Id) + gl.Viewport(0, 0, int32(fbo.Width), int32(fbo.Height)) +} + +func (fbo *Framebuffer) UnBind() { + gl.BindFramebuffer(gl.FRAMEBUFFER, 0) +} + +func (fbo *Framebuffer) UnBindWithViewport(width, height uint32) { + gl.BindFramebuffer(gl.FRAMEBUFFER, 0) + gl.Viewport(0, 0, int32(width), int32(height)) +} + +// IsComplete returns true if OpenGL reports that the fbo is complete/usable. +// Note that this function binds and then unbinds the fbo +func (fbo *Framebuffer) IsComplete() bool { + fbo.Bind() + isComplete := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE + fbo.UnBind() + return isComplete +} + +func (fbo *Framebuffer) HasColorAttachment() bool { + return fbo.ColorAttachmentsCount > 0 +} + +func (fbo *Framebuffer) HasDepthAttachment() bool { + + for i := 0; i < len(fbo.Attachments); i++ { + + a := &fbo.Attachments[i] + if a.Format.IsDepthFormat() { + return true + } + } + + return false +} + +func (fbo *Framebuffer) NewColorAttachment( + attachType FramebufferAttachmentType, + attachFormat FramebufferAttachmentDataFormat, +) { + + if fbo.ColorAttachmentsCount == 8 { + logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due it already having %d attached\n", fbo.ColorAttachmentsCount) + } + + if !attachType.IsValid() { + logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due to unknown attachment type. Type=%d\n", attachType) + } + + if !attachFormat.IsColorFormat() { + logging.ErrLog.Fatalf("failed creating color attachment for framebuffer due to attachment data format not being a valid color type. Data format=%d\n", attachFormat) + } + + a := FramebufferAttachment{ + Type: attachType, + Format: attachFormat, + } + + fbo.Bind() + + if attachType == FramebufferAttachmentType_Texture { + + // Create texture + gl.GenTextures(1, &a.Id) + if a.Id == 0 { + logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError()) + } + + gl.BindTexture(gl.TEXTURE_2D, a.Id) + gl.TexImage2D(gl.TEXTURE_2D, 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.UNSIGNED_BYTE, nil) + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.BindTexture(gl.TEXTURE_2D, 0) + + // Attach to fbo + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0+fbo.ColorAttachmentsCount, gl.TEXTURE_2D, a.Id, 0) + + } else if attachType == FramebufferAttachmentType_Renderbuffer { + + // Create rbo + gl.GenRenderbuffers(1, &a.Id) + if a.Id == 0 { + logging.ErrLog.Fatalf("failed to generate render buffer for framebuffer. GlError=%d\n", gl.GetError()) + } + + gl.BindRenderbuffer(gl.RENDERBUFFER, a.Id) + gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(attachFormat.GlInternalFormat()), int32(fbo.Width), int32(fbo.Height)) + gl.BindRenderbuffer(gl.RENDERBUFFER, 0) + + // Attach to fbo + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0+fbo.ColorAttachmentsCount, gl.RENDERBUFFER, a.Id) + } + + fbo.UnBind() + fbo.ColorAttachmentsCount++ + fbo.Attachments = append(fbo.Attachments, a) +} + +func (fbo *Framebuffer) NewDepthStencilAttachment( + attachType FramebufferAttachmentType, + attachFormat FramebufferAttachmentDataFormat, +) { + + if fbo.HasDepthAttachment() { + logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer because a depth-stencil attachment already exists\n") + } + + if !attachType.IsValid() { + logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer due to unknown attachment type. Type=%d\n", attachType) + } + + if !attachFormat.IsDepthFormat() { + logging.ErrLog.Fatalf("failed creating depth-stencil attachment for framebuffer due to attachment data format not being a valid depth-stencil type. Data format=%d\n", attachFormat) + } + + a := FramebufferAttachment{ + Type: attachType, + Format: attachFormat, + } + + fbo.Bind() + + if attachType == FramebufferAttachmentType_Texture { + + // Create texture + gl.GenTextures(1, &a.Id) + if a.Id == 0 { + logging.ErrLog.Fatalf("failed to generate texture for framebuffer. GlError=%d\n", gl.GetError()) + } + + gl.BindTexture(gl.TEXTURE_2D, a.Id) + gl.TexImage2D(gl.TEXTURE_2D, 0, attachFormat.GlInternalFormat(), int32(fbo.Width), int32(fbo.Height), 0, attachFormat.GlFormat(), gl.UNSIGNED_INT_24_8, nil) + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + gl.BindTexture(gl.TEXTURE_2D, 0) + + // Attach to fbo + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, a.Id, 0) + + } else if attachType == FramebufferAttachmentType_Renderbuffer { + + // Create rbo + gl.GenRenderbuffers(1, &a.Id) + if a.Id == 0 { + logging.ErrLog.Fatalf("failed to generate render buffer for framebuffer. GlError=%d\n", gl.GetError()) + } + + gl.BindRenderbuffer(gl.RENDERBUFFER, a.Id) + gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(attachFormat.GlInternalFormat()), int32(fbo.Width), int32(fbo.Height)) + gl.BindRenderbuffer(gl.RENDERBUFFER, 0) + + // Attach to fbo + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, a.Id) + } + + fbo.UnBind() + fbo.Attachments = append(fbo.Attachments, a) +} + +func (fbo *Framebuffer) Delete() { + + if fbo.Id == 0 { + return + } + + gl.DeleteFramebuffers(1, &fbo.Id) + fbo.Id = 0 +} + +func NewFramebuffer(width, height uint32) Framebuffer { + + // It is allowed to have attachments of differnt sizes in one FBO, + // but that complicates things (e.g. which size to use for gl.viewport) and I don't see much use + // for it now, so we will have all attachments share size + fbo := Framebuffer{ + Width: width, + Height: height, + } + + gl.GenFramebuffers(1, &fbo.Id) + if fbo.Id == 0 { + logging.ErrLog.Fatalf("failed to generate framebuffer. GlError=%d\n", gl.GetError()) + } + + return fbo +} diff --git a/main.go b/main.go index d008ce8..03f3562 100755 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( imgui "github.com/AllenDang/cimgui-go" "github.com/bloeys/gglm/gglm" "github.com/bloeys/nmage/assets" + "github.com/bloeys/nmage/buffers" "github.com/bloeys/nmage/camera" "github.com/bloeys/nmage/engine" "github.com/bloeys/nmage/entity" @@ -99,6 +100,14 @@ var ( yaw float32 = -1.5 cam *camera.Camera + renderToFbo = true + fboRenderDirectly = true + fboScale = gglm.NewVec2(0.25, 0.25) + fboOffset = gglm.NewVec2(0.75, -0.75) + fbo buffers.Framebuffer + + screenQuadMat *materials.Material + unlitMat *materials.Material whiteMat *materials.Material containerMat *materials.Material palleteMat *materials.Material @@ -386,6 +395,15 @@ func (g *Game) Init() { } // Create materials and assign any unused texture slots to black + screenQuadMat = materials.NewMaterial("Screen Quad Mat", "./res/shaders/screen-quad.glsl") + screenQuadMat.SetUnifVec2("scale", fboScale) + screenQuadMat.SetUnifVec2("offset", fboOffset) + screenQuadMat.SetUnifInt32("material.diffuse", 0) + + unlitMat = materials.NewMaterial("Unlit mat", "./res/shaders/simple-unlit.glsl") + unlitMat.SetUnifInt32("material.diffuse", 0) + unlitMat.SetUnifMat4("projMat", &cam.ProjMat) + whiteMat = materials.NewMaterial("White mat", "./res/shaders/simple.glsl") whiteMat.Shininess = 64 whiteMat.DiffuseTex = whiteTex.TexID @@ -449,6 +467,28 @@ func (g *Game) Init() { g.updateLights() updateViewMat() + + g.initFbo() +} + +func (g *Game) initFbo() { + + fbWidth, fbHeight := g.Win.SDLWin.GLGetDrawableSize() + if fbWidth <= 0 || fbHeight <= 0 { + panic("what?") + } + + fbo = buffers.NewFramebuffer(uint32(fbWidth), uint32(fbHeight)) + + fbo.NewColorAttachment( + buffers.FramebufferAttachmentType_Texture, + buffers.FramebufferAttachmentDataFormat_SRGBA, + ) + + fbo.NewDepthStencilAttachment( + buffers.FramebufferAttachmentType_Renderbuffer, + buffers.FramebufferAttachmentDataFormat_Depth24Stencil8, + ) } func (g *Game) updateLights() { @@ -693,6 +733,20 @@ func (g *Game) showDebugWindow() { imgui.EndListBox() } + // Fbo + imgui.Text("Framebuffer") + + imgui.Checkbox("Render to FBO", &renderToFbo) + imgui.Checkbox("Render Directly", &fboRenderDirectly) + + if imgui.DragFloat2("Scale", &fboScale.Data) { + screenQuadMat.SetUnifVec2("scale", fboScale) + } + + if imgui.DragFloat2("Offset", &fboOffset.Data) { + screenQuadMat.SetUnifVec2("offset", fboOffset) + } + // Other imgui.Text("Other Settings") @@ -762,6 +816,11 @@ func (g *Game) updateCameraPos() { func (g *Game) Render() { + if renderToFbo { + fbo.Bind() + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + tempModelMatrix := cubeModelMat.Clone() whiteMat.SetUnifVec3("camPos", &cam.Pos) @@ -806,6 +865,21 @@ func (g *Game) Render() { if drawSkybox { g.DrawSkybox() } + + if renderToFbo { + + fbo.UnBind() + + if fboRenderDirectly { + renderToFbo = false + g.Render() + renderToFbo = true + } + + screenQuadMat.DiffuseTex = fbo.Attachments[0].Id + screenQuadMat.Bind() + gl.DrawArrays(gl.TRIANGLES, 0, 6) + } } func (g *Game) DrawSkybox() { @@ -845,6 +919,7 @@ func (g *Game) DeInit() { func updateViewMat() { cam.Update() + unlitMat.SetUnifMat4("viewMat", &cam.ViewMat) whiteMat.SetUnifMat4("viewMat", &cam.ViewMat) containerMat.SetUnifMat4("viewMat", &cam.ViewMat) palleteMat.SetUnifMat4("viewMat", &cam.ViewMat) diff --git a/res/shaders/screen-quad.glsl b/res/shaders/screen-quad.glsl new file mode 100755 index 0000000..9531b38 --- /dev/null +++ b/res/shaders/screen-quad.glsl @@ -0,0 +1,45 @@ +//shader:vertex +#version 410 + +out vec2 vertUV0; + +// Hardcoded vertex positions for a fullscreen quad. +// Format: vec4(pos.x, pos.y, uv0.x, uv0.y) +vec4 quadData[6] = vec4[]( + vec4(-1.0, 1.0, 0.0, 1.0), + vec4(-1.0, -1.0, 0.0, 0.0), + vec4(1.0, -1.0, 1.0, 0.0), + vec4(-1.0, 1.0, 0.0, 1.0), + vec4(1.0, -1.0, 1.0, 0.0), + vec4(1.0, 1.0, 1.0, 1.0) +); + +uniform vec2 scale = vec2(1, 1); +uniform vec2 offset = vec2(0, 0); + +void main() +{ + vec4 vertData = quadData[gl_VertexID]; + + vertUV0 = vertData.zw; + gl_Position = vec4((vertData.xy * scale) + offset, 0.0, 1.0); +} + +//shader:fragment +#version 410 + +struct Material { + sampler2D diffuse; +}; + +uniform Material material; + +in vec2 vertUV0; + +out vec4 fragColor; + +void main() +{ + vec4 diffuseTexColor = texture(material.diffuse, vertUV0); + fragColor = vec4(diffuseTexColor.rgb, 1); +} diff --git a/res/shaders/simple-unlit.glsl b/res/shaders/simple-unlit.glsl new file mode 100755 index 0000000..51097c7 --- /dev/null +++ b/res/shaders/simple-unlit.glsl @@ -0,0 +1,54 @@ +//shader:vertex +#version 410 + +layout(location=0) in vec3 vertPosIn; +layout(location=1) in vec3 vertNormalIn; +layout(location=2) in vec2 vertUV0In; +layout(location=3) in vec3 vertColorIn; + +out vec3 vertNormal; +out vec2 vertUV0; +out vec3 vertColor; +out vec3 fragPos; + +//MVP = Model View Projection +uniform mat4 modelMat; +uniform mat4 viewMat; +uniform mat4 projMat; + +void main() +{ + // @TODO: Calculate this on the CPU and send it as a uniform + + // This produces the normal matrix that multiplies with the model normal to produce the + // world space normal. Based on 'One last thing' section from: https://learnopengl.com/Lighting/Basic-Lighting + vertNormal = mat3(transpose(inverse(modelMat))) * vertNormalIn; + + vertUV0 = vertUV0In; + vertColor = vertColorIn; + fragPos = vec3(modelMat * vec4(vertPosIn, 1.0)); + + gl_Position = projMat * viewMat * modelMat * vec4(vertPosIn, 1.0); +} + +//shader:fragment +#version 410 + +struct Material { + sampler2D diffuse; +}; + +uniform Material material; + +in vec3 vertColor; +in vec3 vertNormal; +in vec2 vertUV0; +in vec3 fragPos; + +out vec4 fragColor; + +void main() +{ + vec4 diffuseTexColor = texture(material.diffuse, vertUV0); + fragColor = vec4(diffuseTexColor.rgb, 1); +}