#pragma once
#include "shared.h"
#include "renderer_types.h"
struct Frustum {
vec4 planes[6]; // left, right, top, bottom, near, far
};
void set_far_plane(inout Frustum frustum, vec3 camera_position, vec3 camera_forward, f32 max_draw_distance) {
// point on the far plane
vec3 point = camera_position + camera_forward * max_draw_distance;
// plane normal points back toward the camera
vec3 normal = -normalize(camera_forward);
// plane equation: dot(n, x) + d = 0
f32 d = -dot(normal, point);
frustum.planes[5] = vec4(normal, d);
}
Frustum extract_frustum(mat4 view_proj) {
Frustum f;
mat4 rows = transpose(view_proj);
f.planes[0] = rows[3] + rows[0]; // left
f.planes[1] = rows[3] - rows[0]; // right
f.planes[2] = rows[3] + rows[1]; // bottom
f.planes[3] = rows[3] - rows[1]; // top
f.planes[4] = rows[3] - rows[2]; // near (reverse-z)
// placeholder, we'll replace this with our finite culling distance
f.planes[5] = rows[2];
for (uint i = 0; i < 6; ++i)
f.planes[i] /= length(f.planes[i].xyz);
return f;
}
bool sphere_visible_in_frustum(vec3 center, f32 radius, Frustum f) {
for (int i = 0; i < 6; i++) {
f32 dist = dot(f.planes[i].xyz, center) + f.planes[i].w;
if (dist < -radius) {
return false; // fully outside this plane
}
}
return true; // At least partially visible
}
bool is_object_visible(SubmeshGPU part, TransformGPU transform, Frustum frustum) {
vec4 sphere_center_ws4 = vec4(part.local_sphere.xyz, 1.0f) * transform.world;
vec3 sphere_center_ws = sphere_center_ws4.xyz / sphere_center_ws4.w;
vec3 scale_axis =
vec3(length(transform.world[0].xyz), length(transform.world[1].xyz), length(transform.world[2].xyz));
f32 sphere_radius_ws = part.local_sphere.w * max(max(scale_axis.x, scale_axis.y), scale_axis.z);
bool is_vis = sphere_visible_in_frustum(sphere_center_ws, sphere_radius_ws, frustum);
return is_vis;
}
// true if the AABB is entirely behind the depth buffer at its screen projection,
// false if any part of the AABB is visible
bool is_object_occluded(
vec3 local_aabb_min,
vec3 local_aabb_max,
TransformGPU transform,
mat4 view_proj,
Texture2D hiz_tex,
u32 hiz_width,
u32 hiz_height,
u32 hiz_mip_count
) {
vec3 corners[8] = {
vec3(local_aabb_min.x, local_aabb_min.y, local_aabb_min.z),
vec3(local_aabb_max.x, local_aabb_min.y, local_aabb_min.z),
vec3(local_aabb_min.x, local_aabb_max.y, local_aabb_min.z),
vec3(local_aabb_max.x, local_aabb_max.y, local_aabb_min.z),
vec3(local_aabb_min.x, local_aabb_min.y, local_aabb_max.z),
vec3(local_aabb_max.x, local_aabb_min.y, local_aabb_max.z),
vec3(local_aabb_min.x, local_aabb_max.y, local_aabb_max.z),
vec3(local_aabb_max.x, local_aabb_max.y, local_aabb_max.z),
};
vec2 uv_min = vec2(1e30f);
vec2 uv_max = vec2(-1e30f);
f32 nearest_depth = 0.0f;
[unroll]
for (u32 i = 0; i < 8; ++i) {
vec4 clip = (vec4(corners[i], 1.0f) * transform.world) * view_proj;
if (clip.w <= 0.0f)
return false;
vec3 ndc = clip.xyz / clip.w;
vec2 uv = ndc.xy * vec2(0.5f, -0.5f) + vec2(0.5f);
uv_min = min(uv_min, uv);
uv_max = max(uv_max, uv);
nearest_depth = max(nearest_depth, ndc.z);
}
if (uv_max.x < 0.0f || uv_max.y < 0.0f || uv_min.x > 1.0f || uv_min.y > 1.0f)
return false;
uv_min = clamp(uv_min, 0.0f.xx, 1.0f.xx);
uv_max = clamp(uv_max, 0.0f.xx, 1.0f.xx);
vec2 rect_px = (uv_max - uv_min) * vec2(hiz_width, hiz_height);
f32 max_rect = max(rect_px.x, rect_px.y);
// u32 mip = min(u32(floor(log2(max(max_rect, 1.0f)))), hiz_mip_count - 1);
// mip = min(mip, hiz_mip_count - 1);
const u32 MAX_HIZ_MIP = 5;
// u32 mip = min(u32(floor(log2(max(max_rect, 1.0f)))), min(hiz_mip_count - 1, MAX_HIZ_MIP));
//
// ceil(log2) ensures the projected rect fits within <=~2 texels per axis
// at the selected mip, making 4-corner sampling conservative.
// floor(log2) could let the rect span ~3 texels, allowing interior gaps
// where a different texel's far depth gets missed.
u32 mip = min(u32(ceil(log2(max(max_rect, 1.0f)))), min(hiz_mip_count - 1, MAX_HIZ_MIP));
vec2 expand = 0.5f / vec2(max(hiz_width >> mip, 1u), max(hiz_height >> mip, 1u));
uv_min = clamp(uv_min - expand, 0.0f.xx, 1.0f.xx);
uv_max = clamp(uv_max + expand, 0.0f.xx, 1.0f.xx);
uint w, h;
hiz_tex.GetDimensions(w, h);
uint mipW = max(1u, (w + (1u << mip) - 1u) >> mip);
uint mipH = max(1u, (h + (1u << mip) - 1u) >> mip);
uvec2 p0 = min(uvec2(uv_min * vec2(mipW, mipH)), uvec2(mipW - 1, mipH - 1));
uvec2 p1 = min(uvec2(vec2(uv_max.x, uv_min.y) * vec2(mipW, mipH)), uvec2(mipW - 1, mipH - 1));
uvec2 p2 = min(uvec2(vec2(uv_min.x, uv_max.y) * vec2(mipW, mipH)), uvec2(mipW - 1, mipH - 1));
uvec2 p3 = min(uvec2(uv_max * vec2(mipW, mipH)), uvec2(mipW - 1, mipH - 1));
f32 d0 = hiz_tex.Load(ivec3(p0, mip)).r;
f32 d1 = hiz_tex.Load(ivec3(p1, mip)).r;
f32 d2 = hiz_tex.Load(ivec3(p2, mip)).r;
f32 d3 = hiz_tex.Load(ivec3(p3, mip)).r;
f32 hiz_depth = min(min(d0, d1), min(d2, d3));
f32 bias = 0.1; // / 0.0001; // TODO: 0.1 is the near plane
return nearest_depth + bias < hiz_depth;
}
bool sphere_intersects_aabb(vec3 center, f32 radius, vec3 bmin, vec3 bmax) {
vec3 closest = clamp(center, bmin, bmax);
vec3 d = center - closest;
return dot(d, d) <= radius * radius;
}