Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 4 additions & 92 deletions assets/shaders/vulkan/terrain.frag
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ layout(location = 11) in float vAO;
layout(location = 12) in vec4 vClipPosCurrent;
layout(location = 13) in vec4 vClipPosPrev;
layout(location = 14) in float vMaskRadius;
layout(location = 15) in float vEntranceBounce;
layout(location = 16) in vec2 vEntranceDir;
layout(location = 17) in float vLODFade;
layout(location = 15) in float vCloud;
layout(location = 16) in float vLODFade;

layout(location = 0) out vec4 FragColor;

Expand Down Expand Up @@ -58,7 +57,6 @@ const int DEBUG_DIRECT_KEY = 7;
const int DEBUG_SKY_FILL = 8;
const int DEBUG_BLOCK_LIGHT = 9;
const int DEBUG_OUTDOOR_FACTOR = 10;
const int DEBUG_ENTRANCE_BOUNCE = 11;
const int DEBUG_SKYLIGHT = 12;
const int DEBUG_AMBIENT_OCCLUSION = 13;

Expand Down Expand Up @@ -116,7 +114,7 @@ layout(set = 0, binding = 2) uniform ShadowUniforms {
vec4 overlap_starts;
vec4 shadow_texel_sizes;
vec4 shadow_depth_spans;
vec4 shadow_params; // x = light size, y = inverse resolution
vec4 shadow_params; // y = inverse resolution
vec4 fade_params; // x = fade start, y = configured shadow distance
} shadows;

Expand Down Expand Up @@ -162,25 +160,6 @@ float interleavedGradientNoise(vec2 fragCoord) {
return fract(magic.z * fract(dot(fragCoord.xy, magic.xy)));
}

// PCSS blocker search using Poisson disk for better spatial distribution.
// searchRadius is derived from light size and receiver depth in light-space.
float findBlocker(vec2 uv, float zReceiver, int layer, float searchRadius, mat2 rot) {
float blockerDepthSum = 0.0;
int numBlockers = 0;
// Use first 8 Poisson samples for blocker search (cheaper than full 16)
for (int i = 0; i < 8; i++) {
vec2 offset = (rot * poissonDisk16[i]) * searchRadius;
float depth = texture(uShadowMapsRegular, vec3(uv + offset, float(layer))).r;
// Reverse-Z: blockers have GREATER depth than receiver
if (depth > zReceiver + 0.0002) {
blockerDepthSum += depth;
numBlockers++;
}
}
if (numBlockers == 0) return -1.0;
return blockerDepthSum / float(numBlockers);
}

vec3 shadowProjCoords(vec3 fragPosWorld, int layer) {
vec4 fragPosLightSpace = shadows.light_space_matrices[layer] * vec4(fragPosWorld, 1.0);
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
Expand Down Expand Up @@ -334,9 +313,6 @@ float computeShadowCascades(vec3 fragPosWorld, vec3 N, vec3 L, float cascadeDist
const float MAX_ENV_MIP_LEVEL = 8.0;
const float SUN_RADIANCE_TO_IRRADIANCE = 4.0;
const float SUN_VOLUMETRIC_INTENSITY = 3.0;
const float LEGACY_LIGHTING_INTENSITY = 2.5;
const float LOD_LIGHTING_INTENSITY = 1.5;
const float NON_PBR_ROUGHNESS = 0.5;
const vec3 IBL_CLAMP = vec3(3.0);
const float VOLUMETRIC_DENSITY_FACTOR = 0.1;
const float DIELECTRIC_F0 = 0.04;
Expand Down Expand Up @@ -448,12 +424,6 @@ float debugOutdoorFactor(float skyLight) {
return baselineOutdoorFactor(skyLight);
}

float classicLightCurve(float lightLevel) {
float l = clamp(lightLevel, 0.0, 1.0);
if (l <= 0.0) return 0.0;
return 0.075 + 0.925 * pow(l, 1.35);
}

vec3 computeTerrainLighting(vec3 albedo, vec3 N, vec3 V, vec3 L, float roughness, float totalShadow, float skyLight, float skyVisibility, vec3 blockLight, float ao, float ssao, out float directKeyOut, out float skyFillOut, out float blockLightOut, out float outdoorOut) {
float atmosphere = skyVisibilityFactor(skyVisibility);
float nDotL = max(dot(N, L), 0.0);
Expand Down Expand Up @@ -525,62 +495,6 @@ vec4 computeVolumetric(vec3 rayStart, vec3 rayEnd, float dither) {
return vec4(accumulatedScattering, transmittance);
}

vec3 computeSunShafts(vec3 rayStart, vec3 rayEnd, vec3 receiverNormal, float receiverSkyLight, float receiverShadow, float dither) {
if (global.volumetric_params.x < 0.5 || global.shadow_params.w < 0.5 || global.params.w <= 0.02) return vec3(0.0);

vec3 rayDir = rayEnd - rayStart;
float totalDist = length(rayDir);
if (totalDist <= 0.001) return vec3(0.0);
rayDir /= totalDist;

vec3 L = normalize(global.sun_dir.xyz);
float viewScatter = pow(clamp(dot(rayDir, L) * 0.5 + 0.5, 0.0, 1.0), 2.0);
float caveGate = 1.0 - smoothstep(0.08, 0.55, clamp(receiverSkyLight, 0.0, 1.0));
float shadowGate = smoothstep(0.06, 0.70, receiverShadow);
float visibilityGate = caveGate * mix(0.70, 1.0, shadowGate);
float receiverGate = smoothstep(-0.10, 0.55, receiverNormal.y);
visibilityGate *= receiverGate;
if (visibilityGate <= 0.001) return vec3(0.0);

const int steps = 8;
float maxDist = min(totalDist, 80.0);
float stepSize = maxDist / float(steps);
float litAccum = 0.0;
float weightAccum = 0.0;

for (int i = 0; i < steps; i++) {
float t = (float(i) + dither) * stepSize;
vec3 p = rayStart + rayDir * t;
float depth = length(p);
float lit = getVolShadow(p, depth);
float distanceFade = 1.0 - smoothstep(0.0, maxDist, t);
float weight = mix(0.35, 1.0, distanceFade);
litAccum += lit * weight;
weightAccum += weight;
}

float viewBeam = litAccum / max(weightAccum, 0.001);
viewBeam *= mix(0.45, 1.0, viewScatter);

float sunSpill = 0.0;
const int spillSteps = 6;
for (int i = 0; i < spillSteps; i++) {
float t = (float(i) + 0.5 + dither * 0.25) * 2.25;
vec3 p = rayEnd + L * t;
float lit = getVolShadow(p, length(p));
float weight = 1.0 - float(i) / float(spillSteps);
sunSpill += lit * weight;
}
sunSpill /= 3.5;

float beam = viewBeam * 0.65 + sunSpill * 0.18;
beam *= visibilityGate;
beam *= smoothstep(0.03, 0.40, maxDist / 80.0);

vec3 warmSun = mix(global.sun_color.rgb, vec3(1.0, 0.84, 0.52), 0.35);
return warmSun * beam * global.volumetric_params.y * 1.35;
}

void main() {
vec3 color;
float outputAlpha = 1.0;
Expand Down Expand Up @@ -632,7 +546,7 @@ void main() {
vec3 L = normalize(global.sun_dir.xyz);
float skyVisibility = clamp(vSkyLight, 0.0, 1.0);
float atmosphericVisibility = skyVisibilityFactor(skyVisibility);
bool isCloud = vTileID < 0 && vEntranceDir.x > 0.9 && vEntranceDir.y < -0.9;
bool isCloud = vCloud > 0.5;
float cascadeDistance = max(vViewDepth, 0.0);
int layer = selectShadowCascade(vFragPosWorld, cascadeDistance);
float shadowFactor = 0.0;
Expand Down Expand Up @@ -729,8 +643,6 @@ void main() {
color = clamp(vBlockLight, 0.0, 1.0);
} else if (debugChannel < DEBUG_OUTDOOR_FACTOR + 0.5) {
color = vec3(debugOutdoor);
} else if (debugChannel < DEBUG_ENTRANCE_BOUNCE + 0.5) {
color = mix(vec3(0.02, 0.02, 0.02), vec3(0.18, 0.85, 1.0), clamp(vEntranceBounce, 0.0, 1.0));
} else if (debugChannel < DEBUG_SKYLIGHT + 0.5) {
color = vec3(clamp(vSkyLight, 0.0, 1.0));
} else if (debugChannel < DEBUG_AMBIENT_OCCLUSION + 0.5) {
Expand Down
Binary file modified assets/shaders/vulkan/terrain.frag.spv
Binary file not shown.
16 changes: 4 additions & 12 deletions assets/shaders/vulkan/terrain.vert
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ layout(location = 2) in uint aNormal;
layout(location = 3) in vec2 aTexCoord;
layout(location = 4) in uint aPackedMeta;
layout(location = 5) in uint aBlockLight;
layout(location = 6) in uint aEntranceDir;

layout(location = 0) out vec3 vColor;
layout(location = 1) flat out vec3 vNormal;
Expand All @@ -23,9 +22,8 @@ layout(location = 11) out float vAO;
layout(location = 12) out vec4 vClipPosCurrent;
layout(location = 13) out vec4 vClipPosPrev;
layout(location = 14) out float vMaskRadius;
layout(location = 15) out float vEntranceBounce;
layout(location = 16) out vec2 vEntranceDir;
layout(location = 17) out float vLODFade;
layout(location = 15) out float vCloud;
layout(location = 16) out float vLODFade;

layout(set = 0, binding = 0) uniform GlobalUniforms {
mat4 view_proj;
Expand Down Expand Up @@ -126,12 +124,7 @@ void main() {
float((aBlockLight >> 8u) & 0xFFu) / 255.0,
float((aBlockLight >> 16u) & 0xFFu) / 255.0
);
float entranceBounce = float((aBlockLight >> 24u) & 0xFFu) / 255.0;
vec2 entranceDir = vec2(
float(aEntranceDir & 0xFFu) / 255.0,
float((aEntranceDir >> 8u) & 0xFFu) / 255.0
) * 2.0 - 1.0;
if (dot(entranceDir, entranceDir) < 0.02) entranceDir = vec2(0.0);
float cloud = float((aBlockLight >> 24u) & 0xFFu) / 255.0;

vColor = decodedColor * color_override;
vNormal = decodedNormal;
Expand All @@ -140,8 +133,7 @@ void main() {
vDistance = length(worldPos.xyz);
vSkyLight = skylight;
vBlockLight = blocklight;
vEntranceBounce = entranceBounce;
vEntranceDir = entranceDir;
vCloud = cloud;

vFragPosWorld = worldPos.xyz;
// clipPos.w is the positive forward view distance with our reverse-Z projection.
Expand Down
Binary file modified assets/shaders/vulkan/terrain.vert.spv
Binary file not shown.
13 changes: 13 additions & 0 deletions docs/lighting-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Lighting Architecture

Terrain uses one production lighting model: skylight and RGB block light stored in `PackedLight` and propagated by `WorldLightingEngine` across each connected loaded chunk component. Chunk arrival and block mutations trigger reconciliation; unloaded chunks are a propagation frontier.

World generation may seed local light for initial chunk data. Runtime reconciliation is authoritative and removes stale values before meshing. Entrance-bounce storage, directional packing, and its mesh/shader path have been retired; save files contain only `PackedLight`, so no chunk migration is needed.

Terrain vertices carry normalized skylight, RGB block light, and AO. The terrain shader combines these with CSM direct lighting, IBL sky fill, and optional volumetric lighting. Cloud vertices use the packed block-light alpha byte solely as an internal cloud marker.

GPU meshing is disabled until its output matches the CPU vertex stride, lighting inputs, AO, and pass routing. CPU meshing is the only production terrain mesh path.

Use `ZIGCRAFT_DEBUG_SHADER` for deterministic terrain captures. The supported capture channels are documented in `lighting-phase0-baselines.md`; capture the full scene matrix with `scripts/capture_lighting_baselines.sh`.

Shadow quality controls PCF tap count, cascade blending, resolution, and distance. LOW, MEDIUM, HIGH, ULTRA, and EXTREME presets should be evaluated with `zig build benchmark`; the committed baseline and CI benchmark comparison are the regression gate. The CPU/shader shadow uniform block is checked by `scripts/check_shadow_abi.sh` during `zig build test`.
4 changes: 2 additions & 2 deletions docs/shaders/spirv-sizes.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"assets/shaders/vulkan/taa.frag": 9760,
"assets/shaders/vulkan/taa.vert": 1160,
"assets/shaders/vulkan/terrain_debug.frag": 1336,
"assets/shaders/vulkan/terrain.frag": 84752,
"assets/shaders/vulkan/terrain.vert": 11144,
"assets/shaders/vulkan/terrain.frag": 63108,
"assets/shaders/vulkan/terrain.vert": 10500,
"assets/shaders/vulkan/ui.frag": 744,
"assets/shaders/vulkan/ui_tex.frag": 1408,
"assets/shaders/vulkan/ui_tex.vert": 1344,
Expand Down
3 changes: 1 addition & 2 deletions modules/engine-ecs/src/systems/render.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ fn makeWireframeVertex(x: f32, y: f32, z: f32) Vertex {
1.0,
.{ 1.0, 1.0, 1.0 },
1.0,
0.0,
);
}

Expand Down Expand Up @@ -55,7 +54,7 @@ const wireframe_line_vertices = [_]Vertex{
const wireframe_line_vertex_count: u32 = @intCast(wireframe_line_vertices.len);

fn makeSolidVertex(x: f32, y: f32, z: f32) Vertex {
return Vertex.init(.{ x, y, z }, .{ 1.0, 1.0, 1.0 }, .{ 0, 1, 0 }, .{ 0, 0 }, 0xffff, 1.0, .{ 1.0, 1.0, 1.0 }, 1.0, 0.0);
return Vertex.init(.{ x, y, z }, .{ 1.0, 1.0, 1.0 }, .{ 0, 1, 0 }, .{ 0, 0 }, 0xffff, 1.0, .{ 1.0, 1.0, 1.0 }, 1.0);
}

const solid_box_vertices = [_]Vertex{
Expand Down
13 changes: 6 additions & 7 deletions modules/engine-graphics/src/cloud_system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,12 @@ pub const CloudSystem = struct {
}

fn emitQuad(self: *CloudSystem, vertices: *std.ArrayListUnmanaged(rhi.Vertex), a: [3]f32, b: [3]f32, c: [3]f32, d: [3]f32, color: [3]f32, normal: [3]f32) !void {
const cloud_marker = [_]f32{ 1.0, -1.0 };
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(b, color, normal, .{ 1.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(d, color, normal, .{ 0.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initWithEntrance(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0, 0.0, cloud_marker));
try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
try vertices.append(self.allocator, rhi.Vertex.initCloud(b, color, normal, .{ 1.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
try vertices.append(self.allocator, rhi.Vertex.initCloud(d, color, normal, .{ 0.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0));
}

fn uploadMesh(self: *CloudSystem) !void {
Expand Down
27 changes: 0 additions & 27 deletions modules/engine-graphics/src/shadow_system_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ test "ShadowConfig default field values" {
try testing.expectEqual(@as(u8, 12), cfg.pcf_samples);
try testing.expect(cfg.cascade_blend);
try testing.expectEqual(@as(f32, 0.35), cfg.strength);
try testing.expectEqual(@as(f32, 3.0), cfg.light_size);
try testing.expectEqual(@as(f32, 250.0), cfg.caster_distance);
}

Expand All @@ -100,7 +99,6 @@ test "ShadowConfig custom field values" {
.pcf_samples = 16,
.cascade_blend = false,
.strength = 0.5,
.light_size = 5.0,
.caster_distance = 400.0,
};

Expand All @@ -109,31 +107,9 @@ test "ShadowConfig custom field values" {
try testing.expectEqual(@as(u8, 16), cfg.pcf_samples);
try testing.expect(!cfg.cascade_blend);
try testing.expectEqual(@as(f32, 0.5), cfg.strength);
try testing.expectEqual(@as(f32, 5.0), cfg.light_size);
try testing.expectEqual(@as(f32, 400.0), cfg.caster_distance);
}

test "ShadowParams default light_size" {
const params = ShadowParams{
.light_space_matrices = .{Mat4.identity} ** CASCADE_COUNT,
.cascade_splits = .{ 10.0, 50.0, 150.0, 500.0 },
.shadow_texel_sizes = .{ 0.5, 1.0, 2.0, 4.0 },
};

try testing.expectEqual(@as(f32, 3.0), params.light_size);
}

test "ShadowParams custom light_size" {
const params = ShadowParams{
.light_space_matrices = .{Mat4.identity} ** CASCADE_COUNT,
.cascade_splits = .{ 10.0, 50.0, 150.0, 500.0 },
.shadow_texel_sizes = .{ 0.5, 1.0, 2.0, 4.0 },
.light_size = 8.0,
};

try testing.expectEqual(@as(f32, 8.0), params.light_size);
}

test "ShadowParams all cascade splits set correctly" {
const params = ShadowParams{
.light_space_matrices = .{Mat4.identity} ** CASCADE_COUNT,
Expand Down Expand Up @@ -265,14 +241,12 @@ test "IShadowContext wrapper delegates updateUniforms" {
.light_space_matrices = .{Mat4.identity} ** CASCADE_COUNT,
.cascade_splits = .{ 20.0, 80.0, 250.0, 600.0 },
.shadow_texel_sizes = .{ 0.1, 0.2, 0.4, 0.8 },
.light_size = 6.0,
};

try shadow_ctx.updateUniforms(params);

try testing.expectEqual(@as(u32, 1), call_count);
try testing.expect(received_params != null);
try testing.expectEqual(@as(f32, 6.0), received_params.?.light_size);
}

test "ShadowSystemWrapper forwards all operations" {
Expand Down Expand Up @@ -333,7 +307,6 @@ test "ShadowSystemWrapper forwards all operations" {
.light_space_matrices = .{Mat4.identity} ** CASCADE_COUNT,
.cascade_splits = .{ 5.0, 25.0, 100.0, 300.0 },
.shadow_texel_sizes = .{ 0.3, 0.6, 1.2, 2.4 },
.light_size = 4.0,
};
try wrapper.updateUniforms(params);
try testing.expectEqual(@as(u32, 1), update_calls);
Expand Down
17 changes: 1 addition & 16 deletions modules/engine-graphics/src/shadow_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,6 @@ test "ShadowConfig default values" {
try testing.expectEqual(@as(u8, 9), config.pcf_samples);
try testing.expect(config.cascade_blend);
try testing.expectEqual(@as(f32, 0.35), config.strength);
try testing.expectEqual(@as(f32, 3.0), config.light_size);
try testing.expectEqual(@as(f32, 250.0), config.caster_distance);
}

Expand All @@ -394,12 +393,10 @@ test "ShadowParams struct layout" {
.cascade_splits = .{ 10.0, 50.0, 150.0, 500.0 },
.shadow_texel_sizes = .{ 0.5, 1.0, 2.0, 4.0 },
.shadow_depth_spans = .{ 100.0, 200.0, 400.0, 800.0 },
.light_size = 3.0,
};

try testing.expectEqual(@as(f32, 10.0), params.cascade_splits[0]);
try testing.expectEqual(@as(f32, 500.0), params.cascade_splits[3]);
try testing.expectEqual(@as(f32, 3.0), params.light_size);
}

test "ShadowCascades isValid detects non-finite light space matrix" {
Expand Down Expand Up @@ -459,17 +456,6 @@ test "IShadowScene renderShadowPass delegates to vtable" {
try testing.expectEqual(@as(f32, 500.0), tracker.last_config.distance);
}

test "ShadowParams default light_size is 3.0" {
const params = rhi.ShadowParams{
.light_space_matrices = .{Mat4.identity} ** rhi.SHADOW_CASCADE_COUNT,
.cascade_splits = .{ 10.0, 50.0, 150.0, 500.0 },
.shadow_texel_sizes = .{ 0.5, 1.0, 2.0, 4.0 },
.shadow_depth_spans = .{ 100.0, 200.0, 400.0, 800.0 },
};

try testing.expectEqual(@as(f32, 3.0), params.light_size);
}

test "ShadowSystem endPass is safe when pass not active" {
var sys = try ShadowSystem.init(testing.allocator, 1024);
defer sys.deinit(null);
Expand Down Expand Up @@ -529,7 +515,7 @@ test "IShadowScene vtable renderShadowPass is called with correct parameters" {
var custom_matrix = Mat4.identity;
custom_matrix.data[0][0] = 5.0;
const custom_camera = Vec3.init(100.0, -50.0, 200.0);
const custom_config = ShadowConfig{ .strength = 0.5, .light_size = 10.0 };
const custom_config = ShadowConfig{ .strength = 0.5 };

scene.renderShadowPass(custom_matrix, custom_camera, Vec3.zero, Vec3.zero, custom_config);

Expand All @@ -539,5 +525,4 @@ test "IShadowScene vtable renderShadowPass is called with correct parameters" {
try testing.expectEqual(@as(f32, -50.0), verify.camera_received.y);
try testing.expectEqual(@as(f32, 200.0), verify.camera_received.z);
try testing.expectEqual(@as(f32, 0.5), verify.config_received.strength);
try testing.expectEqual(@as(f32, 10.0), verify.config_received.light_size);
}
Loading
Loading