// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
priv struct ShadowAtlasRect {
offset : @smath.Vec2
scale : @smath.Vec2
}
///|
priv struct FrameSpotShadowState {
light_view_projection : @raylib.Matrix
atlas_rect : ShadowAtlasRect
depth_bias : Double
normal_bias : Double
}
///|
priv struct FramePointShadowState {
face_matrices : Array[@raylib.Matrix]
face_rects : Array[ShadowAtlasRect]
depth_bias : Double
normal_bias : Double
}
///|
const SPOT_SHADOW_TEXTURE_SLOT : Int = 10
///|
const POINT_SHADOW_TEXTURE_SLOT : Int = 11
///|
const SPOT_SHADOW_ATLAS_COLUMNS : Int = 2
///|
const SPOT_SHADOW_ATLAS_ROWS : Int = 2
///|
const POINT_SHADOW_FACE_COUNT : Int = 6
///|
const POINT_SHADOW_ATLAS_COLUMNS : Int = 8
///|
const POINT_SHADOW_ATLAS_ROWS : Int = 6
///|
const LOCAL_SHADOW_NEAR_MIN : Double = 0.01
///|
fn make_shadow_atlas_rect(
slot : Int,
columns : Int,
tile_size : Int,
atlas_width : Int,
atlas_height : Int,
) -> ShadowAtlasRect {
let column = slot % columns
let row = slot / columns
let tile_width = tile_size.to_double() / atlas_width.to_double()
let tile_height = tile_size.to_double() / atlas_height.to_double()
{
offset: Vec2(column.to_double() * tile_width, row.to_double() * tile_height),
scale: Vec2(tile_width, tile_height),
}
}
///|
fn uniform_shadow_atlas_rect(
rect : ShadowAtlasRect,
) -> @raylib.ShaderUniformData {
Vec4(
@raylib.Vector4::new(
to_float(rect.offset[X]),
to_float(rect.offset[Y]),
to_float(rect.scale[X]),
to_float(rect.scale[Y]),
),
)
}
///|
fn point_shadow_texel_size(face_size : Int) -> @smath.Vec2 {
let inv = 1.0 / @cmp.maximum(1, face_size).to_double()
Vec2(inv, inv)
}
///|
fn spot_shadow_texel_size(shadow_map_size : Int) -> @smath.Vec2 {
let inv = 1.0 / @cmp.maximum(1, shadow_map_size).to_double()
Vec2(inv, inv)
}
///|
fn shadow_map_near_z(near_z : Double) -> Double {
@cmp.maximum(LOCAL_SHADOW_NEAR_MIN, near_z)
}
///|
fn build_spot_shadow_setup(
light : @render3d_types.FrameSpotLight3D,
) -> DirectionalShadowSetup {
let direction = normalize_or(light.direction, Vec3(0.0, -1.0, 0.0))
let target = light.position + direction
let near_plane = shadow_map_near_z(light.shadow_map_near_z)
let far_plane = @cmp.maximum(near_plane + 1.0, light.range)
let light_view = @raylib.Matrix::look_at(
to_ray_vector3_smath(light.position),
to_ray_vector3_smath(target),
to_ray_vector3_smath(choose_shadow_up(direction)),
)
let light_projection = @raylib.Matrix::perspective(
@cmp.maximum(light.outer_angle * 2.0, 0.017453292519943295),
1.0,
near_plane,
far_plane,
)
{
light_view_projection: @raylib.Matrix::multiply(
light_view, light_projection,
),
half_extent: 0.0,
near_plane,
far_plane,
near_bound: near_plane,
far_bound: far_plane,
}
}
///|
fn point_shadow_face(index : Int) -> (@smath.Vec3, @smath.Vec3) {
match index {
0 => (Vec3(1.0, 0.0, 0.0), Vec3(0.0, -1.0, 0.0))
1 => (Vec3(-1.0, 0.0, 0.0), Vec3(0.0, -1.0, 0.0))
2 => (Vec3(0.0, 1.0, 0.0), Vec3(0.0, 0.0, 1.0))
3 => (Vec3(0.0, -1.0, 0.0), Vec3(0.0, 0.0, -1.0))
4 => (Vec3(0.0, 0.0, 1.0), Vec3(0.0, -1.0, 0.0))
_ => (Vec3(0.0, 0.0, -1.0), Vec3(0.0, -1.0, 0.0))
}
}
///|
fn build_point_shadow_face_setup(
light : @render3d_types.FramePointLight3D,
face_index : Int,
) -> DirectionalShadowSetup {
let (direction, up) = point_shadow_face(face_index)
let near_plane = shadow_map_near_z(light.shadow_map_near_z)
let far_plane = @cmp.maximum(near_plane + 1.0, light.range)
let light_view = @raylib.Matrix::look_at(
to_ray_vector3_smath(light.position),
to_ray_vector3_smath(light.position + direction),
to_ray_vector3_smath(up),
)
let light_projection = @raylib.Matrix::perspective(
1.5707963267948966, 1.0, near_plane, far_plane,
)
{
light_view_projection: @raylib.Matrix::multiply(
light_view, light_projection,
),
half_extent: 0.0,
near_plane,
far_plane,
near_bound: near_plane,
far_bound: far_plane,
}
}
///|
fn get_spot_shadow_render_texture(
shadow_map_size : Int,
) -> @raylib.RenderTexture? {
let shadow_map_size = @cmp.maximum(1, shadow_map_size)
if backend.spot_shadow_map_size != shadow_map_size {
if backend.spot_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.spot_shadow_render_texture = None
backend.spot_shadow_map_size = shadow_map_size
}
if backend.spot_shadow_render_texture is Some(existing) {
if existing.id() > 0U && @raylib.get_render_texture_depth_id(existing) > 0U {
return Some(existing)
}
return None
}
let width = shadow_map_size * SPOT_SHADOW_ATLAS_COLUMNS
let height = shadow_map_size * SPOT_SHADOW_ATLAS_ROWS
let fbo_id = @rl.load_framebuffer()
let depth_tex_id = @rl.load_texture_depth(width, height, false)
@rl.framebuffer_attach(fbo_id, depth_tex_id, 100, 100, 0)
ignore(@rl.framebuffer_complete(fbo_id))
let render_texture = @raylib.RenderTexture::new(
fbo_id,
@raylib.Texture::new(0U, width, height, 1, 7),
@raylib.Texture::new(depth_tex_id, width, height, 1, 19),
)
let depth_id = @raylib.get_render_texture_depth_id(render_texture)
if render_texture.id() == 0U || depth_id == 0U {
warn_texture_issue(
"spot_shadow_atlas_unavailable", "raylib spot-light shadow atlas framebuffer could not be created; spot shadows are disabled",
)
render_texture.unload()
return None
}
let depth_texture = @raylib.Texture::new(
depth_id,
width,
height,
1,
@raylib.PixelformatUncompressedR8g8b8a8,
)
@raylib.set_texture_filter(depth_texture, @raylib.TextureFilterPoint)
@raylib.set_texture_wrap(depth_texture, @raylib.TextureWrapClamp)
backend.spot_shadow_render_texture = Some(render_texture)
Some(render_texture)
}
///|
fn get_point_shadow_render_texture(face_size : Int) -> @raylib.RenderTexture? {
let face_size = @cmp.maximum(1, face_size)
if backend.point_shadow_map_size != face_size {
if backend.point_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.point_shadow_render_texture = None
backend.point_shadow_map_size = face_size
}
if backend.point_shadow_render_texture is Some(existing) {
if existing.id() > 0U && @raylib.get_render_texture_depth_id(existing) > 0U {
return Some(existing)
}
return None
}
let width = face_size * POINT_SHADOW_ATLAS_COLUMNS
let height = face_size * POINT_SHADOW_ATLAS_ROWS
let fbo_id = @rl.load_framebuffer()
let depth_tex_id = @rl.load_texture_depth(width, height, false)
@rl.framebuffer_attach(fbo_id, depth_tex_id, 100, 100, 0)
ignore(@rl.framebuffer_complete(fbo_id))
let render_texture = @raylib.RenderTexture::new(
fbo_id,
@raylib.Texture::new(0U, width, height, 1, 7),
@raylib.Texture::new(depth_tex_id, width, height, 1, 19),
)
let depth_id = @raylib.get_render_texture_depth_id(render_texture)
if render_texture.id() == 0U || depth_id == 0U {
warn_texture_issue(
"point_shadow_atlas_unavailable", "raylib point-light shadow atlas framebuffer could not be created; point shadows are disabled",
)
render_texture.unload()
return None
}
let depth_texture = @raylib.Texture::new(
depth_id,
width,
height,
1,
@raylib.PixelformatUncompressedR8g8b8a8,
)
@raylib.set_texture_filter(depth_texture, @raylib.TextureFilterPoint)
@raylib.set_texture_wrap(depth_texture, @raylib.TextureWrapClamp)
backend.point_shadow_render_texture = Some(render_texture)
Some(render_texture)
}
///|
fn shadow_caster_items(
frame : @render3d_types.RenderFrame3D,
) -> Array[@render3d_types.RenderItem3D] {
let items : Array[@render3d_types.RenderItem3D] = []
for item in frame.items {
if item.cast_shadows {
items.push(item)
}
}
items
}
///|
fn render_shadow_casters_for_setup(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
setup : DirectionalShadowSetup,
) -> Unit {
begin_3d(camera)
for item in shadow_caster_items(frame) {
guard backend.meshes3d.get(item.mesh) is Some(mesh_asset) else { continue }
let material = backend.materials3d
.get(item.material)
.unwrap_or(@render3d_types.default_standard_material3d())
draw_shadow_mesh3d_instance(
item.mesh,
mesh_asset,
material,
item.transform,
setup,
)
}
end_3d()
}
///|
fn render_spot_shadow_map(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
) -> (Map[Int, FrameSpotShadowState], UInt?, @smath.Vec2) {
let spot_shadows : Map[Int, FrameSpotShadowState] = Map([])
let spot_count = @cmp.minimum(frame.spot_lights.length(), MAX_SPOT_LIGHTS)
let shadow_map_size = @cmp.maximum(1, frame.directional_shadow_map_size)
if spot_count == 0 {
return (spot_shadows, None, Vec2(0.0, 0.0))
}
guard get_spot_shadow_render_texture(shadow_map_size) is Some(render_texture) else {
return (spot_shadows, None, Vec2(0.0, 0.0))
}
let atlas_width = shadow_map_size * SPOT_SHADOW_ATLAS_COLUMNS
let atlas_height = shadow_map_size * SPOT_SHADOW_ATLAS_ROWS
@raylib.begin_texture_mode(render_texture)
@raylib.clear_background(to_ray_color(white_render_color()))
for light_index in 0.. (Map[Int, FramePointShadowState], UInt?, @smath.Vec2) {
let point_shadows : Map[Int, FramePointShadowState] = Map([])
let point_count = @cmp.minimum(frame.point_lights.length(), MAX_POINT_LIGHTS)
let face_size = @cmp.maximum(1, frame.point_shadow_map_size)
if point_count == 0 {
return (point_shadows, None, Vec2(0.0, 0.0))
}
guard get_point_shadow_render_texture(face_size) is Some(render_texture) else {
return (point_shadows, None, Vec2(0.0, 0.0))
}
let atlas_width = face_size * POINT_SHADOW_ATLAS_COLUMNS
let atlas_height = face_size * POINT_SHADOW_ATLAS_ROWS
@raylib.begin_texture_mode(render_texture)
@raylib.clear_background(to_ray_color(white_render_color()))
for light_index in 0..