// 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.Vec2D
scale : @smath.Vec2D
}
///|
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: @smath.Vec2D(
column.to_double() * tile_width,
row.to_double() * tile_height,
),
scale: @smath.Vec2D(tile_width, tile_height),
}
}
///|
fn bytes_from_shadow_atlas_rect(rect : ShadowAtlasRect) -> Bytes {
let out : Array[Byte] = []
append_f32_le(out, rect.offset[X])
append_f32_le(out, rect.offset[Y])
append_f32_le(out, rect.scale[X])
append_f32_le(out, rect.scale[Y])
Bytes::from_array(out)
}
///|
fn point_shadow_texel_size(face_size : Int) -> @smath.Vec2D {
let inv = 1.0 / @cmp.maximum(1, face_size).to_double()
@smath.Vec2D(inv, inv)
}
///|
fn spot_shadow_texel_size(shadow_map_size : Int) -> @smath.Vec2D {
let inv = 1.0 / @cmp.maximum(1, shadow_map_size).to_double()
@smath.Vec2D(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,
@smath.Vec3::new(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 => (@smath.Vec3::new(1.0, 0.0, 0.0), @smath.Vec3::new(0.0, -1.0, 0.0))
1 => (@smath.Vec3::new(-1.0, 0.0, 0.0), @smath.Vec3::new(0.0, -1.0, 0.0))
2 => (@smath.Vec3::new(0.0, 1.0, 0.0), @smath.Vec3::new(0.0, 0.0, 1.0))
3 => (@smath.Vec3::new(0.0, -1.0, 0.0), @smath.Vec3::new(0.0, 0.0, -1.0))
4 => (@smath.Vec3::new(0.0, 0.0, 1.0), @smath.Vec3::new(0.0, -1.0, 0.0))
_ => (@smath.Vec3::new(0.0, 0.0, -1.0), @smath.Vec3::new(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) {
@raylib.unload_shadowmap_render_texture(render_texture)
}
backend.spot_shadow_render_texture = None
backend.spot_shadow_map_size = shadow_map_size
}
if backend.spot_shadow_render_texture is Some(existing) {
if @raylib.get_render_texture_fbo_id(existing) > 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 render_texture = @raylib.load_shadowmap_render_texture(width, height)
let depth_id = @raylib.get_render_texture_depth_id(render_texture)
if @raylib.get_render_texture_fbo_id(render_texture) == 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",
)
@raylib.unload_shadowmap_render_texture(render_texture)
return None
}
let depth_texture = @raylib.texture_from_id(depth_id, width, height)
@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) {
@raylib.unload_shadowmap_render_texture(render_texture)
}
backend.point_shadow_render_texture = None
backend.point_shadow_map_size = face_size
}
if backend.point_shadow_render_texture is Some(existing) {
if @raylib.get_render_texture_fbo_id(existing) > 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 render_texture = @raylib.load_shadowmap_render_texture(width, height)
let depth_id = @raylib.get_render_texture_depth_id(render_texture)
if @raylib.get_render_texture_fbo_id(render_texture) == 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",
)
@raylib.unload_shadowmap_render_texture(render_texture)
return None
}
let depth_texture = @raylib.texture_from_id(depth_id, width, height)
@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(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.Vec2D) {
let spot_shadows : Map[Int, FrameSpotShadowState] = Map::new()
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, @smath.Vec2D(0.0, 0.0))
}
guard get_spot_shadow_render_texture(shadow_map_size) is Some(render_texture) else {
return (spot_shadows, None, @smath.Vec2D(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.Vec2D) {
let point_shadows : Map[Int, FramePointShadowState] = Map::new()
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, @smath.Vec2D(0.0, 0.0))
}
guard get_point_shadow_render_texture(face_size) is Some(render_texture) else {
return (point_shadows, None, @smath.Vec2D(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..