Use view frustum to calculate directional light projection, add a bunch of debug stuff for frustums

This commit is contained in:
sergeypdev 2024-03-11 19:57:25 +04:00
parent 6b4fe69505
commit bb8b29263f
7 changed files with 218 additions and 24 deletions

38
assets/cube.obj Normal file
View File

@ -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

34
assets/shaders/unlit.glsl Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
{
"shader": "unlit.glsl",
"vertex": true,
"fragment": true
}

View File

@ -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 {

View File

@ -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(),

View File

@ -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) {

View File

@ -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);