diff --git a/assets/cube.obj b/assets/cube.obj new file mode 100644 index 0000000..b1f9c3c --- /dev/null +++ b/assets/cube.obj @@ -0,0 +1,38 @@ +# Blender 4.0.2 +# www.blender.org +o Cube +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 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.875000 0.500000 +vt 0.875000 0.750000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.625000 1.000000 +vt 0.375000 1.000000 +vt 0.375000 0.000000 +vt 0.625000 0.000000 +vt 0.625000 0.250000 +vt 0.375000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.500000 +vt 0.125000 0.750000 +s 0 +f 1/1/1 5/2/1 7/3/1 3/4/1 +f 4/5/2 3/4/2 7/6/2 8/7/2 +f 8/8/3 7/9/3 5/10/3 6/11/3 +f 6/12/4 2/13/4 4/5/4 8/14/4 +f 2/13/5 1/1/5 3/4/5 4/5/5 +f 6/11/6 5/10/6 1/1/6 2/13/6 diff --git a/assets/shaders/unlit.glsl b/assets/shaders/unlit.glsl new file mode 100644 index 0000000..23e9f6c --- /dev/null +++ b/assets/shaders/unlit.glsl @@ -0,0 +1,34 @@ +#extension GL_ARB_bindless_texture : enable + +// UBOs +layout(std140, binding = 0) uniform Matrices { + mat4 projection; + mat4 view; +}; + +// Uniforms +layout(location = 1) uniform mat4 model; + +layout(location = 2) uniform vec3 color; + +// Input, output blocks + +#if VERTEX_SHADER + +layout(location = 0) in vec3 aPos; + +void main() { + gl_Position = projection * view * model * vec4(aPos.xyz, 1.0); +} +#endif // VERTEX_SHADER + +#if FRAGMENT_SHADER + +out vec4 FragColor; + +void main() { + FragColor = vec4(color, 1.0f); +} + + +#endif // FRAGMNET_SHADER diff --git a/assets/shaders/unlit.prog b/assets/shaders/unlit.prog new file mode 100644 index 0000000..c9b77a8 --- /dev/null +++ b/assets/shaders/unlit.prog @@ -0,0 +1,5 @@ +{ + "shader": "unlit.glsl", + "vertex": true, + "fragment": true +} diff --git a/src/AssetManager.zig b/src/AssetManager.zig index 0b5bb71..3c958e1 100644 --- a/src/AssetManager.zig +++ b/src/AssetManager.zig @@ -590,6 +590,10 @@ pub const IndexSlice = struct { offset: gl.GLuint, count: gl.GLsizei, type: gl.GLenum, + + pub fn bind(self: *const IndexSlice) void { + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buffer); + } }; pub const ShaderType = enum { diff --git a/src/Render.zig b/src/Render.zig index b4da2df..5e0b033 100644 --- a/src/Render.zig +++ b/src/Render.zig @@ -19,6 +19,7 @@ pub const MAX_FRAMES_QUEUED = 3; pub const MAX_LIGHTS = 8; pub const MAX_DRAW_COMMANDS = 4096; pub const MAX_LIGHT_COMMANDS = 2048; +pub const CSM_SPLITS = 4; pub const Render = @This(); @@ -63,6 +64,11 @@ post_process_vao: gl.GLuint = 0, // Bloom screen_bloom_sampler: gl.GLuint = 0, +update_view_frustum: bool = true, +camera_view_proj: Mat4 = Mat4.identity(), +world_camera_frustum: math.Frustum = .{}, +world_view_frustum_corners: [8]Vec3 = [_]Vec3{Vec3.new(0, 0, 0)} ** 8, + pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetman: *AssetManager) Render { var render = Render{ .allocator = allocator, @@ -165,7 +171,7 @@ pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetm checkGLError(); std.debug.assert(render.shadow_texture_array != 0); - gl.textureStorage3D(render.shadow_texture_array, 1, gl.DEPTH_COMPONENT16, 2048, 2048, 1); + gl.textureStorage3D(render.shadow_texture_array, 1, gl.DEPTH_COMPONENT16, 2048, 2048, CSM_SPLITS); checkGLError(); gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE); @@ -431,6 +437,22 @@ pub fn draw(self: *Render, cmd: DrawCommand) void { pub fn finish(self: *Render) void { const ginit = globals.g_init; + const camera_projection = self.camera.projection(); + const view_proj = camera_projection.mul(self.camera.view_mat); + if (self.update_view_frustum) { + self.camera_view_proj = view_proj; + self.world_camera_frustum = math.Frustum.new(view_proj); + } + + const inv_view_proj = view_proj.inv(); + + if (self.update_view_frustum) { + for (math.ndc_box_corners, 0..) |corner, i| { + const pos4 = inv_view_proj.mulByVec4(corner.toVec4(1)); + self.world_view_frustum_corners[i] = pos4.toVec3().scale(1.0 / pos4.w()); + } + } + const lights = self.lights[0..self.light_count]; // Sort lights: directional first @@ -449,6 +471,10 @@ pub fn finish(self: *Render) void { const lights_buf = self.getLightBuffer(); lights_buf.count = 0; + var dir_aabb_min = Vec3.zero(); + var dir_aabb_max = Vec3.zero(); + var dir_view_proj_mat = Mat4.identity(); + // Light shadow maps { gl.bindVertexArray(self.shadow_vao); @@ -476,17 +502,32 @@ pub fn finish(self: *Render) void { gl.viewport(0, 0, 2048, 2048); + var projection: Mat4 = undefined; + const view = Mat4.lookAt( + dir_light.dir.scale(-1), + Vec3.zero(), + Vec3.up(), + ); + + { + for (self.world_view_frustum_corners) |corner| { + const pos4 = view.mulByVec4(corner.toVec4(1)); + const pos = pos4.toVec3(); + dir_aabb_min = pos.min(dir_aabb_min); + dir_aabb_max = pos.max(dir_aabb_max); + } + projection = math.orthographic(dir_aabb_min.x(), dir_aabb_max.x(), dir_aabb_min.y(), dir_aabb_max.y(), -dir_aabb_max.z(), -dir_aabb_min.z()); + //projection = math.orthographic(-1, 1, -5, 5, 0, 0.5); + } + const camera_matrix = &self.shadow_matrices; camera_matrix.* = .{ - .projection = math.orthographic(-4, 4, -4, 4, -5, 5), - .view = Mat4.lookAt( - dir_light.dir.scale(-1), - Vec3.zero(), - Vec3.up(), - ), + .view = view, + .projection = projection, }; - const shadow_view_proj = camera_matrix.projection.mul(camera_matrix.view); + const shadow_view_proj = projection.mul(view); + dir_view_proj_mat = shadow_view_proj; const light_frustum = math.Frustum.new(shadow_view_proj); light.shadow_vp = shadow_view_proj; @@ -578,12 +619,10 @@ pub fn finish(self: *Render) void { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - const projection = self.camera.projection(); - { const camera_matrix: *CameraMatrices = @alignCast(@ptrCast(self.camera_matrices[self.tripple_buffer_index * self.uboAlignedSizeOf(CameraMatrices) ..].ptr)); camera_matrix.* = .{ - .projection = projection, + .projection = camera_projection, .view = self.camera.view_mat, }; @@ -601,15 +640,12 @@ pub fn finish(self: *Render) void { gl.useProgram(self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.mesh).program); gl.bindVertexArray(self.mesh_vao); - const view_proj = projection.mul(self.camera.view_mat); - const world_camera_frustum = math.Frustum.new(view_proj); - var rendered_count: usize = 0; for (self.command_buffer[0..self.command_count]) |*cmd| { const mesh = self.assetman.resolveMesh(cmd.mesh); const aabb = math.AABB.fromMinMax(mesh.aabb.min, mesh.aabb.max); - if (!world_camera_frustum.intersectAABB(aabb.transform(cmd.transform))) { + if (!self.world_camera_frustum.intersectAABB(aabb.transform(cmd.transform))) { continue; } rendered_count += 1; @@ -681,6 +717,55 @@ pub fn finish(self: *Render) void { ); } + // Debug stuff + { + gl.polygonMode(gl.FRONT_AND_BACK, gl.LINE); + defer gl.polygonMode(gl.FRONT_AND_BACK, gl.FILL); + gl.lineWidth(4); + + // Frustum debug stuff, drawn only when view frustum is fixed + if (!self.update_view_frustum) { + gl.useProgram(self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.unlit).program); + + // Draw wire frustum cubes + { + const mesh = self.assetman.resolveMesh(a.Meshes.cube.Cube); + mesh.positions.bind(Render.Attrib.Position.value()); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indices.buffer); + gl.uniform3fv(Uniform.Color.value(), 1, @ptrCast(&Vec3.one().data)); + + const model = Mat4.fromTranslate(Vec3.new(0, 0, 0.5)).mul(Mat4.fromScale(Vec3.new(1, 1, 0.5))); + + const view_proj_matrices = [_]Mat4{ self.camera_view_proj, dir_view_proj_mat }; + + for (view_proj_matrices) |frustum_view_proj| { + const frustum_model_mat = frustum_view_proj.inv().mul(model); + gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&frustum_model_mat.data)); + gl.drawElements( + gl.TRIANGLES, + mesh.indices.count, + mesh.indices.type, + @ptrFromInt(mesh.indices.offset), + ); + } + } + // Draw corner positions of view frustum + { + const mesh = self.assetman.resolveMesh(a.Meshes.sphere.Icosphere); + mesh.positions.bind(Attrib.Position.value()); + mesh.indices.bind(); + + gl.uniform3fv(Uniform.Color.value(), 1, @ptrCast(&Vec3.new(1, 0, 0).data)); + + for (self.world_view_frustum_corners) |corner| { + const model = Mat4.fromTranslate(corner); + gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&model.data)); + gl.drawElements(gl.TRIANGLES, mesh.indices.count, mesh.indices.type, @ptrFromInt(mesh.indices.offset)); + } + } + } + } + //std.log.debug("Total draws {}, frustum culled draws {}\n", .{ self.command_count, rendered_count }); gl.disable(gl.DEPTH_TEST); @@ -927,7 +1012,7 @@ pub const Camera = struct { fovy: f32 = 60, aspect: f32 = 1, near: f32 = 0.1, - far: f32 = 100, + far: f32 = 10, view_mat: Mat4 = Mat4.identity(), diff --git a/src/game.zig b/src/game.zig index a3a6ac6..a8b6c01 100644 --- a/src/game.zig +++ b/src/game.zig @@ -335,6 +335,22 @@ export fn game_update() bool { ginit.vsync = !ginit.vsync; } }, + // Freeze view frustum + c.SDL_SCANCODE_F8 => { + if (event.type == c.SDL_KEYDOWN) { + gmem.render.update_view_frustum = !gmem.render.update_view_frustum; + } + }, + // Expand camera far + c.SDL_SCANCODE_F7 => { + if (event.type == c.SDL_KEYDOWN) { + if (gmem.render.camera.far == 10) { + gmem.render.camera.far = 100; + } else { + gmem.render.camera.far = 10; + } + } + }, c.SDL_SCANCODE_ESCAPE => { if (event.type == c.SDL_KEYUP) { if (ginit.fullscreen) { diff --git a/src/math.zig b/src/math.zig index 035ea03..d4fd935 100644 --- a/src/math.zig +++ b/src/math.zig @@ -18,6 +18,18 @@ pub const box_corners = [8]Vec3{ Vec3.new(1, 1, 1), }; +// Using DirectX/Vulkan NDC coordinates +pub const ndc_box_corners = [8]Vec3{ + Vec3.new(-1, -1, 0), + Vec3.new(-1, -1, 1), + Vec3.new(-1, 1, 0), + Vec3.new(-1, 1, 1), + Vec3.new(1, -1, 0), + Vec3.new(1, -1, 1), + Vec3.new(1, 1, 0), + Vec3.new(1, 1, 1), +}; + pub const Plane = struct { // x, y, z - normal, w - distance nd: Vec4 = Vec4.up(), @@ -74,12 +86,12 @@ pub const AABB = struct { pub const Frustum = struct { // Plane normals - top: Plane, - right: Plane, - bottom: Plane, - left: Plane, - near: Plane, - far: Plane, + top: Plane = .{}, + right: Plane = .{}, + bottom: Plane = .{}, + left: Plane = .{}, + near: Plane = .{}, + far: Plane = .{}, /// Extracts frustum planes from matrices using Gribb-Hartmann method /// If you pass in a projection matrix planes will be in view space. @@ -95,7 +107,7 @@ pub const Frustum = struct { const right = row4.sub(row1); const bottom = row4.add(row2); const top = row4.sub(row2); - const near = row4.add(row3); + const near = row3; const far = row4.sub(row3); return .{ @@ -195,7 +207,7 @@ pub fn orthographic(left: f32, right: f32, bottom: f32, top: f32, z_near: f32, z result.data[0][0] = 2 / (right - left); result.data[1][1] = 2 / (top - bottom); - result.data[2][2] = 2 / (z_near - z_far); + result.data[2][2] = 1 / (z_near - z_far); result.data[3][3] = 1; result.data[3][0] = (left + right) / (left - right);