Point light shadow maps with filtering, improve bias with normal offset

This commit is contained in:
sergeypdev 2024-03-02 23:23:46 +04:00
parent daf85fc614
commit 95d30aacda
6 changed files with 293 additions and 94 deletions

View File

@ -0,0 +1,38 @@
// UBOs
layout(std140, binding = 0) uniform Matrices {
mat4 projection;
mat4 view;
};
// Uniforms
layout(location = 1) uniform mat4 model;
layout(location = 18) uniform vec2 near_far;
// Input, output blocks
VERTEX_EXPORT VertexData {
vec3 vPos;
} VertexOut;
#if VERTEX_SHADER
layout(location = 0) in vec3 aPos;
void main() {
vec4 vPos = view * model * vec4(aPos.xyz, 1.0);
gl_Position = projection * vPos;
VertexOut.vPos = vPos.xyz;
}
#endif // VERTEX_SHADER
#if FRAGMENT_SHADER
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
void main() {
gl_FragDepth = map(length(VertexOut.vPos), near_far.x, near_far.y, 0, 1);
}
#endif // FRAGMNET_SHADER

View File

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

View File

@ -7,6 +7,8 @@
struct Light {
vec4 vPos;
vec4 color;
mat4 shadow_vp; // for spot and dir lights
vec2 near_far; // for point lights
};
// UBOs
@ -44,7 +46,7 @@ layout(location = 14, bindless_sampler) uniform sampler2D emission_map;
layout(location = 15) uniform vec2 emission_map_uv_scale = vec2(1);
layout(location = 16, bindless_sampler) uniform sampler2DArrayShadow shadow_maps;
layout(location = 17) uniform mat4 shadow_map_vp;
layout(location = 17, bindless_sampler) uniform samplerCubeArrayShadow cube_shadow_maps;
// Input, output blocks
@ -54,6 +56,7 @@ VERTEX_EXPORT VertexData {
vec2 uv;
mat3 vTBN;
vec3 wPos;
vec3 wNormal;
} VertexOut;
float random(vec4 seed4) {
@ -81,6 +84,7 @@ void main() {
VertexOut.vTBN = mat3(T, B, N);
vec4 wPos = model * vec4(aPos.xyz, 1.0);
VertexOut.wPos = wPos.xyz / wPos.w;
VertexOut.wNormal = normalize(model * vec4(aNormal, 0.0)).xyz;
}
#endif // VERTEX_SHADER
@ -145,7 +149,11 @@ vec2 poissonDisk[4] = vec2[](
vec2( 0.34495938, 0.29387760 )
);
vec3 microfacetModel(Material mat, Light light, vec3 P, vec3 N) {
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
vec3 microfacetModel(Material mat, int light_idx, Light light, vec3 P, vec3 N) {
vec3 diffuseBrdf = vec3(0); // metallic
if (!mat.metallic) {
diffuseBrdf = mat.albedo;
@ -177,37 +185,60 @@ vec3 microfacetModel(Material mat, Light light, vec3 P, vec3 N) {
float NDotL = max(dot(N, L), 0);
float NDotV = dot(N, V);
float normal_offset_scale = clamp(1 - NDotL, 0, 1);
normal_offset_scale *= 10; // constant
float constant_bias = 0.001;
float shadow_mult = 1;
//// TODO: Shadows for directional light only for now
if (point == 0) {
vec4 shadow_pos = shadow_map_vp * vec4(VertexOut.wPos, 1.0);
vec4 shadow_offset = vec4(VertexOut.wNormal * normal_offset_scale, 0);
if (point == 1) {
vec2 shadow_map_texel_size = 1.0 / vec2(textureSize(cube_shadow_maps, 0));
shadow_offset *= shadow_map_texel_size.x;
vec3 shadow_dir = (light.shadow_vp * vec4(VertexOut.wPos, 1.0)).xyz;
float world_depth = length(shadow_dir.xyz);
shadow_dir = normalize((light.shadow_vp * (vec4(VertexOut.wPos, 1.0) + shadow_offset)).xyz);
float mapped_depth = map(world_depth, light.near_far.x, light.near_far.y, 0, 1);
vec4 texcoord;
texcoord.xyz = shadow_dir;
texcoord.w = float(light_idx);
float sum = 0;
for (float z = -1; z <= 1; z += 1) {
for (float y = -1; y <= 1; y += 1) {
for (float x = -1; x <= 1; x += 1) {
sum += texture(cube_shadow_maps, vec4(normalize(texcoord.xyz + vec3(x, y, z) * shadow_map_texel_size.x), texcoord.w), mapped_depth - constant_bias);
}
}
}
shadow_mult = sum / 27;
} else {
vec2 shadow_map_texel_size = 1.0 / vec2(textureSize(shadow_maps, 0));
shadow_offset *= shadow_map_texel_size.x;
// Directional shadow
vec4 shadow_pos = light.shadow_vp * vec4(VertexOut.wPos, 1.0);
shadow_pos.xy = (light.shadow_vp * (vec4(VertexOut.wPos, 1.0) + shadow_offset)).xy;
shadow_pos /= shadow_pos.w;
shadow_pos.xyz = shadow_pos.xyz * 0.5 + 0.5; // [-1, 1] to [0, 1]
float bias = 0.005 * tan(acos(NDotL));
shadow_pos.z -= bias;
shadow_pos.z -= constant_bias;
vec4 texcoord;
texcoord.xyw = shadow_pos.xyz; // sampler2DArrayShadow strange texcoord mapping
texcoord.z = 0; // First shadow map
float sum = 0;
vec2 tex_scale = 1.0 / vec2(textureSize(shadow_maps, 0));
for (float y = -1.5; y <= 1.5; y += 1) {
for (float x = -1.5; x <= 1.5; x += 1) {
sum += texture(shadow_maps, vec4(texcoord.xy + vec2(x, y) * tex_scale, texcoord.zw));
sum += texture(shadow_maps, vec4(texcoord.xy + vec2(x, y) * shadow_map_texel_size, texcoord.zw));
}
}
shadow_mult = sum / 16.0;
// for (int i=0; i<4; i++){
// int index = int(16.0 * random(vec4(VertexOut.wPos.xyz * 1000.0, i))) % 4;
// float depth_test = texture(shadow_maps, vec4(texcoord.xy + poissonDisk[index]/700.0, texcoord.zw));
// shadow_mult -= 0.25 * (1 - depth_test);
// }
//shadow_mult = texture(shadow_maps, texcoord);
}
shadow_mult = clamp(shadow_mult, 0, 1);
vec3 specBrdf = 0.25 * ggxDistribution(mat, NDotH) * schlickFresnel(mat, LDotH) * geomSmith(mat, NDotL) * geomSmith(mat, NDotV);
@ -226,7 +257,7 @@ void main() {
vec3 finalColor = vec3(0);
for (int i = 0; i < lights_count; i++) {
finalColor += microfacetModel(material, lights[i], VertexOut.vPos, N);
finalColor += microfacetModel(material, i, lights[i], VertexOut.vPos, N);
}
FragColor = vec4(finalColor, 1.0f);

View File

@ -20,8 +20,9 @@ void main() {
#if FRAGMENT_SHADER
void main() {
gl_FragDepth = gl_FragCoord.z / gl_FragCoord.w;
//gl_FragDepth = gl_FragCoord.z / gl_FragCoord.w;
}

View File

@ -36,11 +36,14 @@ command_buffer: [MAX_DRAW_COMMANDS]DrawCommand = undefined,
command_count: usize = 0,
ubo_align: usize = 0,
shadow_vao: gl.GLuint = 0,
shadow_texture_arrray: gl.GLuint = 0,
shadow_texture_array: gl.GLuint = 0,
shadow_texture_handle: gl.GLuint64 = 0,
shadow_framebuffer: gl.GLuint = 0,
shadow_matrices_buffer: gl.GLuint = 0,
shadow_matrices: CameraMatrices = .{},
cube_shadow_texture_array: gl.GLuint = 0,
cube_shadow_texture_handle: gl.GLuint64 = 0,
cube_shadow_framebuffer: gl.GLuint = 0,
pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetman: *AssetManager) Render {
var render = Render{
@ -136,34 +139,68 @@ pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetm
}
{
// Shadow texture array
gl.createTextures(gl.TEXTURE_2D_ARRAY, 1, &render.shadow_texture_arrray);
checkGLError();
std.debug.assert(render.shadow_texture_arrray != 0);
// 2D Shadow texture array
{
gl.createTextures(gl.TEXTURE_2D_ARRAY, 1, &render.shadow_texture_array);
checkGLError();
std.debug.assert(render.shadow_texture_array != 0);
gl.textureStorage3D(render.shadow_texture_arrray, 1, gl.DEPTH_COMPONENT16, 2048, 2048, 1);
checkGLError();
gl.textureStorage3D(render.shadow_texture_array, 1, gl.DEPTH_COMPONENT16, 2048, 2048, 1);
checkGLError();
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_COMPARE_FUNC, gl.LESS);
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER);
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER);
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.textureParameteri(render.shadow_texture_arrray, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.textureParameterfv(render.shadow_texture_arrray, gl.TEXTURE_BORDER_COLOR, @ptrCast(&Vec4.one().data));
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_COMPARE_FUNC, gl.LESS);
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER);
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER);
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.textureParameterfv(render.shadow_texture_array, gl.TEXTURE_BORDER_COLOR, @ptrCast(&Vec4.one().data));
}
// First shadow texture handle
render.shadow_texture_handle = gl.GL_ARB_bindless_texture.getTextureHandleARB(render.shadow_texture_arrray);
checkGLError();
gl.GL_ARB_bindless_texture.makeTextureHandleResidentARB(render.shadow_texture_handle);
checkGLError();
{
render.shadow_texture_handle = gl.GL_ARB_bindless_texture.getTextureHandleARB(render.shadow_texture_array);
checkGLError();
std.debug.assert(render.shadow_texture_handle != 0);
gl.GL_ARB_bindless_texture.makeTextureHandleResidentARB(render.shadow_texture_handle);
checkGLError();
}
// Cube Shadow texture array
{
gl.createTextures(gl.TEXTURE_CUBE_MAP_ARRAY, 1, &render.cube_shadow_texture_array);
checkGLError();
std.debug.assert(render.cube_shadow_texture_array != 0);
gl.textureStorage3D(render.cube_shadow_texture_array, 1, gl.DEPTH_COMPONENT16, 512, 512, MAX_POINT_LIGHTS * 6);
checkGLError();
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_COMPARE_FUNC, gl.LESS);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.textureParameteri(render.cube_shadow_texture_array, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
}
// Cube Shadow array handle
{
render.cube_shadow_texture_handle = gl.GL_ARB_bindless_texture.getTextureHandleARB(render.cube_shadow_texture_array);
checkGLError();
std.debug.assert(render.cube_shadow_texture_handle != 0);
gl.GL_ARB_bindless_texture.makeTextureHandleResidentARB(render.cube_shadow_texture_handle);
checkGLError();
}
// Shadow FBO
gl.createFramebuffers(1, &render.shadow_framebuffer);
checkGLError();
std.debug.assert(render.shadow_framebuffer != 0);
{
gl.createFramebuffers(1, &render.shadow_framebuffer);
checkGLError();
std.debug.assert(render.shadow_framebuffer != 0);
}
gl.namedFramebufferTextureLayer(render.shadow_framebuffer, gl.DEPTH_ATTACHMENT, render.shadow_texture_arrray, 0, 0);
gl.namedFramebufferTextureLayer(render.shadow_framebuffer, gl.DEPTH_ATTACHMENT, render.shadow_texture_array, 0, 0);
const check_fbo_status = gl.checkNamedFramebufferStatus(render.shadow_framebuffer, gl.DRAW_FRAMEBUFFER);
if (check_fbo_status != gl.FRAMEBUFFER_COMPLETE) {
std.log.debug("Shadow Framebuffer Incomplete: {}\n", .{check_fbo_status});
@ -251,61 +288,88 @@ pub fn draw(self: *Render, cmd: DrawCommand) void {
pub fn finish(self: *Render) void {
const ginit = globals.g_init;
var dir_light: ?PointLight = null;
var dir_light_vp: ?Mat4 = null;
const lights = self.getPointLights();
// Find directional light
// Light shadow maps
{
const lights = self.getPointLights();
gl.bindVertexArray(self.shadow_vao);
for (lights.lights[0..lights.count]) |*light| {
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, self.shadow_framebuffer);
for (lights.lights[0..lights.count], 0..) |*light, i| {
gl.useProgram(self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.shadow).program);
// Directional light
if (std.math.approxEqAbs(f32, light.pos.w(), 0, std.math.floatEps(f32))) {
dir_light = light.*;
}
gl.namedFramebufferTextureLayer(self.shadow_framebuffer, gl.DEPTH_ATTACHMENT, self.shadow_texture_array, 0, 0);
const check_fbo_status = gl.checkNamedFramebufferStatus(self.shadow_framebuffer, gl.DRAW_FRAMEBUFFER);
if (check_fbo_status != gl.FRAMEBUFFER_COMPLETE) {
std.log.debug("Shadow Framebuffer Incomplete: {}\n", .{check_fbo_status});
}
// Transform view to camera view space
light.pos = self.camera.view_mat.mulByVec4(light.pos);
gl.viewport(0, 0, 2048, 2048);
const camera_matrix = &self.shadow_matrices;
camera_matrix.* = .{
.projection = Mat4.orthographic(-2, 2, -2, 2, -5, 5),
.view = Mat4.lookAt(
Vec3.new(light.pos.x(), light.pos.y(), light.pos.z()).scale(-1),
Vec3.zero(),
Vec3.up(),
),
};
light.shadow_vp = camera_matrix.projection.mul(camera_matrix.view);
gl.namedBufferSubData(self.shadow_matrices_buffer, 0, @sizeOf(CameraMatrices), std.mem.asBytes(&self.shadow_matrices));
checkGLError();
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.bindBufferBase(gl.UNIFORM_BUFFER, UBO.CameraMatrices.value(), self.shadow_matrices_buffer);
self.renderShadow();
} else {
// Point Light
gl.useProgram(self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.cube_shadow).program);
const pos = Vec3.new(light.pos.x(), light.pos.y(), light.pos.z());
light.shadow_vp = Mat4.fromTranslate(pos.negate());
// For each cube face
for (cube_camera_dirs, 0..) |cam_dir, face| {
gl.namedFramebufferTextureLayer(self.shadow_framebuffer, gl.DEPTH_ATTACHMENT, self.cube_shadow_texture_array, 0, @intCast(i * 6 + face));
const check_fbo_status = gl.checkNamedFramebufferStatus(self.shadow_framebuffer, gl.DRAW_FRAMEBUFFER);
if (check_fbo_status != gl.FRAMEBUFFER_COMPLETE) {
std.log.debug("Shadow Framebuffer Incomplete: {}\n", .{check_fbo_status});
}
gl.viewport(0, 0, 512, 512);
const near_far = Vec2.new(0.1, 10);
const camera_matrix = &self.shadow_matrices;
camera_matrix.* = .{
.projection = Mat4.perspective(90, 1, near_far.x(), near_far.y()),
.view = Mat4.lookAt(
pos,
pos.add(cam_dir.target),
cam_dir.up,
),
};
light.near_far = near_far;
gl.uniform2f(Uniform.NearFarPlanes.value(), near_far.x(), near_far.y());
gl.namedBufferSubData(self.shadow_matrices_buffer, 0, @sizeOf(CameraMatrices), std.mem.asBytes(&self.shadow_matrices));
checkGLError();
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.bindBufferBase(gl.UNIFORM_BUFFER, UBO.CameraMatrices.value(), self.shadow_matrices_buffer);
self.renderShadow();
}
}
}
}
// Directional Light shadow map
if (dir_light) |light| {
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, self.shadow_framebuffer);
gl.viewport(0, 0, 2048, 2048);
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.useProgram(self.assetman.resolveShaderProgram(a.ShaderPrograms.shaders.shadow).program);
gl.bindVertexArray(self.shadow_vao);
const camera_matrix = &self.shadow_matrices;
camera_matrix.* = .{
.projection = Mat4.orthographic(-2, 2, -2, 2, -5, 5),
.view = Mat4.lookAt(
Vec3.new(light.pos.x(), light.pos.y(), light.pos.z()).scale(-1),
Vec3.zero(),
Vec3.up(),
),
};
dir_light_vp = camera_matrix.projection.mul(camera_matrix.view);
// TODO: use multiple buffers for this in the future
gl.namedBufferSubData(self.shadow_matrices_buffer, 0, @sizeOf(CameraMatrices), std.mem.asBytes(camera_matrix));
checkGLError();
gl.bindBufferBase(gl.UNIFORM_BUFFER, UBO.CameraMatrices.value(), self.shadow_matrices_buffer);
for (self.command_buffer[0..self.command_count]) |*cmd| {
const mesh = self.assetman.resolveMesh(cmd.mesh);
gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&cmd.transform.data));
mesh.positions.bind(Render.Attrib.Position.value());
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indices.buffer);
gl.drawElements(
gl.TRIANGLES,
mesh.indices.count,
mesh.indices.type,
@ptrFromInt(mesh.indices.offset),
);
}
// Light world space to view space
for (lights.lights[0..lights.count]) |*light| {
light.pos = self.camera.view_mat.mulByVec4(light.pos);
}
var width: c_int = 0;
@ -393,10 +457,8 @@ pub fn finish(self: *Render) void {
);
gl.uniform2fv(Uniform.EmissionMapUVScale.value(), 1, @ptrCast(&emission_map.uv_scale.data));
}
if (dir_light != null) {
gl.GL_ARB_bindless_texture.uniformHandleui64ARB(Uniform.ShadowMap.value(), self.shadow_texture_handle);
gl.uniformMatrix4fv(Uniform.ShadowMapVP.value(), 1, gl.FALSE, @ptrCast(&dir_light_vp.?.data));
}
gl.GL_ARB_bindless_texture.uniformHandleui64ARB(Uniform.ShadowMap2D.value(), self.shadow_texture_handle);
gl.GL_ARB_bindless_texture.uniformHandleui64ARB(Uniform.ShadowMapCube.value(), self.cube_shadow_texture_handle);
mesh.positions.bind(Render.Attrib.Position.value());
mesh.normals.bind(Render.Attrib.Normal.value());
@ -416,6 +478,62 @@ pub fn finish(self: *Render) void {
c.SDL_Delay(1);
}
const CubeCameraDir = struct {
face: gl.GLenum,
target: Vec3,
up: Vec3,
};
const cube_camera_dirs = [6]CubeCameraDir{
.{
.face = gl.TEXTURE_CUBE_MAP_POSITIVE_X,
.target = Vec3.right(),
.up = Vec3.down(),
},
.{
.face = gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
.target = Vec3.left(),
.up = Vec3.down(),
},
.{
.face = gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
.target = Vec3.up(),
.up = Vec3.forward(),
},
.{
.face = gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
.target = Vec3.down(),
.up = Vec3.back(),
},
.{
.face = gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
.target = Vec3.forward(),
.up = Vec3.down(),
},
.{
.face = gl.TEXTURE_CUBE_MAP_NEGATIVE_Z,
.target = Vec3.back(),
.up = Vec3.down(),
},
};
fn renderShadow(self: *Render) void {
for (self.command_buffer[0..self.command_count]) |*cmd| {
const mesh = self.assetman.resolveMesh(cmd.mesh);
gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&cmd.transform.data));
mesh.positions.bind(Render.Attrib.Position.value());
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, mesh.indices.buffer);
gl.drawElements(
gl.TRIANGLES,
mesh.indices.count,
mesh.indices.type,
@ptrFromInt(mesh.indices.offset),
);
}
}
pub fn checkGLError() void {
var err = gl.getError();
if (err == gl.NO_ERROR) return;
@ -483,8 +601,10 @@ pub const Uniform = enum(gl.GLint) {
EmissionMap = 14,
EmissionMapUVScale = 15,
ShadowMap = 16,
ShadowMapVP = 17,
ShadowMap2D = 16,
ShadowMapCube = 17,
NearFarPlanes = 18, // vec2 stores near and far planes for perspective projection
pub inline fn value(self: Uniform) gl.GLint {
return @intFromEnum(self);
@ -513,6 +633,8 @@ const CameraMatrices = extern struct {
pub const PointLight = extern struct {
pos: Vec4, // x, y, z, w - vPos
color_radius: Vec4, // x, y, z - color, w - radius
shadow_vp: Mat4 = Mat4.identity(),
near_far: Vec2 = Vec2.zero(),
};
// TODO: rename

View File

@ -114,6 +114,7 @@ fn loadGL() void {
};
gl.debugMessageCallback(glDebugCallback, null);
// gl.enable(gl.DEBUG_OUTPUT);
gl.enable(gl.DEBUG_OUTPUT_SYNCHRONOUS);
}
fn glDebugCallback(source: gl.GLenum, _type: gl.GLenum, id: gl.GLuint, severity: gl.GLenum, length: gl.GLsizei, message: [*:0]const u8, userParam: ?*anyopaque) callconv(.C) void {
@ -188,7 +189,7 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
_ = globals.g_mem.world.addEntity(.{
.flags = .{ .dir_light = true, .rotate = true },
.transform = .{ .rot = Quat.fromEulerAngles(Vec3.new(60, 15, 0)) },
.light = .{ .color_intensity = Vec4.new(1, 1, 0.83, 1) },
.light = .{ .color_intensity = Vec4.new(1, 1, 0.83, 0.7) },
});
const light_root = globals.g_mem.world.addEntity(.{