///|
pub fn sync_image_asset(
handle : @render3d_types.ImageHandle,
image : @render3d_types.ImageAsset,
) -> Unit {
backend.images3d.set(handle, image)
if image_handle_requires_mipmaps(handle) {
ensure_texture_mipmaps(get_texture(image.path))
}
}
///|
pub fn sync_cubemap_asset(
handle : @render3d_types.CubemapHandle,
cubemap : @render3d_types.CubemapAsset3D,
) -> Unit {
backend.cubemaps3d.set(handle, cubemap)
}
///|
pub fn release_mesh_asset(handle : @render3d_types.MeshHandle) -> Unit {
clear_triangle_mesh_cache_for_handle(handle)
backend.meshes3d.remove(handle)
}
///|
pub fn release_material_asset(handle : @render3d_types.MaterialHandle) -> Unit {
backend.materials3d.remove(handle)
}
///|
pub fn release_image_asset(handle : @render3d_types.ImageHandle) -> Unit {
backend.images3d.remove(handle)
}
///|
pub fn release_cubemap_asset(handle : @render3d_types.CubemapHandle) -> Unit {
backend.cubemaps3d.remove(handle)
}
///|
pub fn render3d_submit(frame : @render3d_types.RenderFrame3D) -> Unit {
guard frame.camera is Some(active_camera) else { return }
let shadow_state = render_directional_shadow_map(frame, active_camera)
if !backend.frame_has_draw_commands {
@raylib.clear_background(to_ray_color(frame.clear_color))
}
mark_frame_drawn()
begin_3d(active_camera)
draw_skybox3d(frame, active_camera)
for item in frame.items {
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_mesh3d_instance(
item.mesh,
mesh_asset,
material,
item.transform,
frame,
active_camera.position,
item.receive_shadows,
shadow_state,
)
}
draw_render3d_lines(frame)
end_3d()
}
///|
fn draw_render3d_lines(frame : @render3d_types.RenderFrame3D) -> Unit {
for line in frame.lines {
@raylib.draw_line_3d(
to_ray_vector3_smath(line.start),
to_ray_vector3_smath(line.end),
to_ray_color(line.color),
)
}
}
///|
fn draw_skybox3d(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
) -> Unit {
guard frame.skybox is Some(skybox) else { return }
guard backend.cubemaps3d.get(skybox.cubemap) is Some(cubemap) else { return }
match cubemap {
SixFaces(faces) => {
let half_size = skybox_half_size(camera)
@rl.disable_depth_mask()
@rl.disable_backface_culling()
draw_skybox_face(
faces.positive_x,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(1.0, -1.0, -1.0),
Vec3(1.0, -1.0, 1.0),
Vec3(1.0, 1.0, 1.0),
Vec3(1.0, 1.0, -1.0),
)
draw_skybox_face(
faces.negative_x,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(-1.0, -1.0, 1.0),
Vec3(-1.0, -1.0, -1.0),
Vec3(-1.0, 1.0, -1.0),
Vec3(-1.0, 1.0, 1.0),
)
draw_skybox_face(
faces.positive_y,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(-1.0, 1.0, -1.0),
Vec3(1.0, 1.0, -1.0),
Vec3(1.0, 1.0, 1.0),
Vec3(-1.0, 1.0, 1.0),
)
draw_skybox_face(
faces.negative_y,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(-1.0, -1.0, 1.0),
Vec3(1.0, -1.0, 1.0),
Vec3(1.0, -1.0, -1.0),
Vec3(-1.0, -1.0, -1.0),
)
draw_skybox_face(
faces.positive_z,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(1.0, -1.0, 1.0),
Vec3(-1.0, -1.0, 1.0),
Vec3(-1.0, 1.0, 1.0),
Vec3(1.0, 1.0, 1.0),
)
draw_skybox_face(
faces.negative_z,
camera.position,
skybox.rotation,
skybox.brightness,
half_size,
Vec3(-1.0, -1.0, -1.0),
Vec3(1.0, -1.0, -1.0),
Vec3(1.0, 1.0, -1.0),
Vec3(-1.0, 1.0, -1.0),
)
@rl.enable_backface_culling()
@rl.enable_depth_mask()
}
}
}
///|
fn skybox_half_size(camera : @render3d_types.FrameCamera3D) -> Double {
let safe_far = if camera.far > 0.001 { camera.far } else { 0.001 }
let max_visible_half = safe_far * 0.45
match camera.projection {
Perspective => max_visible_half
Orthographic =>
camera.orthographic_size.map_or(max_visible_half, fn(size) {
let cover_half = @cmp.maximum(size[X].abs(), size[Y].abs()) * 0.5
if cover_half > max_visible_half {
max_visible_half
} else if cover_half <= 0.001 {
max_visible_half
} else {
cover_half
}
})
}
}
///|
fn draw_skybox_face(
image : @render3d_types.ImageHandle,
camera_position : @smath.Vec3,
rotation : @smath.Quat,
brightness : Double,
half_size : Double,
p0 : @smath.Vec3,
p1 : @smath.Vec3,
p2 : @smath.Vec3,
p3 : @smath.Vec3,
) -> Unit {
guard backend.images3d.get(image) is Some(image_asset) else { return }
let texture = get_texture(image_asset.path)
let color = clamp_u8((255.0 * @cmp.maximum(0.0, brightness)).to_int()).to_byte()
@rl.set_texture(texture.id())
@rl.color4ub(color, color, color, b'\xFF')
@rl.begin(@rl.Triangles)
emit_skybox_vertex(camera_position, rotation, half_size, p0, 0.0, 1.0)
emit_skybox_vertex(camera_position, rotation, half_size, p1, 1.0, 1.0)
emit_skybox_vertex(camera_position, rotation, half_size, p2, 1.0, 0.0)
emit_skybox_vertex(camera_position, rotation, half_size, p0, 0.0, 1.0)
emit_skybox_vertex(camera_position, rotation, half_size, p2, 1.0, 0.0)
emit_skybox_vertex(camera_position, rotation, half_size, p3, 0.0, 0.0)
@rl.end_()
@rl.set_texture(0U)
}
///|
fn emit_skybox_vertex(
camera_position : @smath.Vec3,
rotation : @smath.Quat,
half_size : Double,
direction : @smath.Vec3,
u : Double,
v : Double,
) -> Unit {
let position = camera_position +
rotation.rotate_vec3(direction.scalar_mul(half_size))
@rl.tex_coord2f(to_float(u), to_float(v))
@rl.vertex3f(to_float(position.x), to_float(position.y), to_float(position.z))
}
///|
fn draw_mesh3d_instance(
mesh_handle : @render3d_types.MeshHandle,
mesh_asset : @render3d_types.MeshAsset,
material : @render3d_types.StandardMaterial3D,
transform : @render3d_types.FrameTransform3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
@rl.push_matrix()
@rl.translatef(
to_float(transform.translation.x),
to_float(transform.translation.y),
to_float(transform.translation.z),
)
apply_quat_rotation(transform.rotation)
@rl.scalef(
to_float(transform.scale.x),
to_float(transform.scale.y),
to_float(transform.scale.z),
)
let texture_bundle = resolve_material_texture_bundle(material)
match mesh_asset.primitive {
Cube(size) =>
draw_lit_cuboid(
size, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
Sphere(radius) =>
draw_lit_sphere(
radius, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
Cylinder(radius_top, radius_bottom, height, slices) =>
draw_lit_cylinder(
radius_top, radius_bottom, height, slices, material, frame, camera_position,
texture_bundle, receive_shadows, shadow_state,
)
Plane(size) => {
let thickness = if size.y.abs() < 0.0001 { 0.02 } else { size.y }
let cuboid = @smath.Vec3(size.x, thickness, size.z)
draw_lit_cuboid(
cuboid, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
}
Triangles(mesh) =>
draw_triangle_mesh(
mesh_handle, mesh, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
}
@rl.pop_matrix()
}
///|
const TEXTURED_SPHERE_STACKS : Int = 16
///|
const TEXTURED_SPHERE_SLICES : Int = 24
///|
fn draw_lit_cuboid(
size : @smath.Vec3,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let key = "cube:\{size.x}:\{size.y}:\{size.z}"
let mesh = get_or_create_primitive_mesh(key, fn() {
@raylib.gen_mesh_cube(to_float(size.x), to_float(size.y), to_float(size.z))
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn draw_lit_sphere(
radius : Double,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let key = "sphere:\{radius}:\{TEXTURED_SPHERE_SLICES}:\{TEXTURED_SPHERE_STACKS}"
let mesh = get_or_create_primitive_mesh(key, fn() {
@raylib.gen_mesh_sphere(
to_float(radius),
TEXTURED_SPHERE_SLICES,
TEXTURED_SPHERE_STACKS,
)
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn draw_lit_cylinder(
radius_top : Double,
radius_bottom : Double,
height : Double,
slices : Int,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let safe_slices = @cmp.maximum(3, slices)
let key = "truncated_cylinder:\{radius_top}:\{radius_bottom}:\{height}:\{safe_slices}"
let mesh = get_or_create_primitive_mesh(key, fn() {
let (vertices, uvs, normals) = build_cylinder_mesh(
radius_top, radius_bottom, height, safe_slices,
)
match
create_uploaded_triangle_mesh(
vertices,
Some(uvs),
None,
normals,
None,
None,
) {
Some(mesh) => mesh
None => empty_uploaded_triangle_mesh()
}
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn resolve_material_texture_source(
binding : @render3d_types.TextureBinding3D,
) -> MaterialTextureSource3D? {
guard backend.images3d.get(binding.image) is Some(image_asset) else {
warn_texture_issue(
"missing_image_handle:\{image_handle_text(binding.image)}",
"material texture handle \{image_handle_text(binding.image)} is not synced; fallback to default texture",
)
return None
}
let texture = get_texture(image_asset.path)
apply_texture_sampler(texture, binding.sampler)
let texture_id = @raylib.get_texture_id(texture)
let width = @raylib.get_texture_width(texture)
let height = @raylib.get_texture_height(texture)
if texture_id <= 0 || width <= 0 || height <= 0 {
warn_texture_issue(
"invalid_texture:\{image_asset.path}",
"failed to load texture '\{image_asset.path}' (id=\{texture_id}, size=\{width}x\{height}); fallback to base_color",
)
return None
}
Some({
texture,
texcoord_set: binding.texcoord_set,
transform: binding.transform,
})
}
///|
fn resolve_material_texture_bundle(
material : @render3d_types.StandardMaterial3D,
) -> MaterialTextureBundle3D? {
let base_source = material.base_color_texture.bind(
resolve_material_texture_source,
)
let emissive_source = material.emissive_texture.bind(
resolve_material_texture_source,
)
let metallic_roughness_source = material.metallic_roughness_texture.bind(
resolve_material_texture_source,
)
let occlusion_source = material.occlusion_texture.bind(
resolve_material_texture_source,
)
let normal_source = material.normal_texture.bind(
resolve_material_texture_source,
)
if base_source is None &&
emissive_source is None &&
metallic_roughness_source is None &&
occlusion_source is None &&
normal_source is None {
return None
}
let default_transform = @render3d_types.default_texture_transform3d()
Some({
base_texture: base_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
emissive_texture: emissive_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
metallic_roughness_texture: metallic_roughness_source.map_or(
get_white_texture(),
fn(source) { source.texture },
),
occlusion_texture: occlusion_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
normal_texture: normal_source.map_or(null_texture(), fn(source) {
source.texture
}),
has_base_texture: base_source is Some(_),
has_emissive_texture: emissive_source is Some(_),
has_metallic_roughness_texture: metallic_roughness_source is Some(_),
has_occlusion_texture: occlusion_source is Some(_),
has_normal_texture: normal_source is Some(_),
base_transform: base_source.map_or(default_transform, fn(source) {
source.transform
}),
emissive_transform: emissive_source.map_or(default_transform, fn(source) {
source.transform
}),
metallic_roughness_transform: metallic_roughness_source.map_or(
default_transform,
fn(source) { source.transform },
),
occlusion_transform: occlusion_source.map_or(default_transform, fn(source) {
source.transform
}),
normal_transform: normal_source.map_or(default_transform, fn(source) {
source.transform
}),
base_texcoord_set: base_source.map_or(0, fn(source) { source.texcoord_set }),
emissive_texcoord_set: emissive_source.map_or(0, fn(source) {
source.texcoord_set
}),
metallic_roughness_texcoord_set: metallic_roughness_source.map_or(0, fn(
source,
) {
source.texcoord_set
}),
occlusion_texcoord_set: occlusion_source.map_or(0, fn(source) {
source.texcoord_set
}),
normal_texcoord_set: normal_source.map_or(0, fn(source) {
source.texcoord_set
}),
})
}
///|
fn texture_wrap_to_raylib(wrap : @render3d_types.TextureWrap3D) -> Int {
match wrap {
ClampToEdge => @raylib.TextureWrapClamp
MirroredRepeat => @raylib.TextureWrapMirrorRepeat
Repeat => @raylib.TextureWrapRepeat
}
}
///|
fn texture_filter_to_raylib(sampler : @render3d_types.TextureSampler3D) -> Int {
let nearest = sampler.mag_filter == Nearest || sampler.min_filter == Nearest
match sampler.mipmap_filter {
Some(_) =>
if nearest {
@raylib.TextureFilterPoint
} else {
@raylib.TextureFilterTrilinear
}
None =>
if nearest {
@raylib.TextureFilterPoint
} else {
@raylib.TextureFilterBilinear
}
}
}
///|
fn sampler_requests_generated_mipmaps(
sampler : @render3d_types.TextureSampler3D,
) -> Bool {
sampler.mipmap_filter is Some(_) &&
texture_filter_to_raylib(sampler) == @raylib.TextureFilterTrilinear
}
///|
fn maybe_ensure_binding_mipmaps(
binding : @render3d_types.TextureBinding3D,
) -> Unit {
if !sampler_requests_generated_mipmaps(binding.sampler) {
return
}
if backend.images3d.get(binding.image) is Some(image_asset) {
ensure_texture_mipmaps(get_texture(image_asset.path))
}
}
///|
fn ensure_material_mipmaps(
material : @render3d_types.StandardMaterial3D,
) -> Unit {
if material.base_color_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.normal_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.emissive_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.metallic_roughness_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.occlusion_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
}
///|
fn material_uses_image_with_mipmaps(
material : @render3d_types.StandardMaterial3D,
image : @render3d_types.ImageHandle,
) -> Bool {
let binding_matches = fn(binding : @render3d_types.TextureBinding3D) -> Bool {
binding.image == image &&
sampler_requests_generated_mipmaps(binding.sampler)
}
let uses_base = match material.base_color_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_normal = match material.normal_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_emissive = match material.emissive_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_metallic_roughness = match material.metallic_roughness_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_occlusion = match material.occlusion_texture {
Some(binding) => binding_matches(binding)
None => false
}
uses_base ||
uses_normal ||
uses_emissive ||
uses_metallic_roughness ||
uses_occlusion
}
///|
fn image_handle_requires_mipmaps(image : @render3d_types.ImageHandle) -> Bool {
for material_handle, material in backend.materials3d {
ignore(material_handle)
if material_uses_image_with_mipmaps(material, image) {
return true
}
}
false
}
///|
fn apply_texture_sampler(
texture : @raylib.Texture,
sampler : @render3d_types.TextureSampler3D,
) -> Unit {
@raylib.set_texture_wrap(texture, texture_wrap_to_raylib(sampler.wrap_u))
@raylib.set_texture_filter(texture, texture_filter_to_raylib(sampler))
}
///|
fn build_cylinder_mesh(
radius_top : Double,
radius_bottom : Double,
height : Double,
slices : Int,
) -> (Array[@smath.Vec3], Array[@smath.Vec2], Array[@smath.Vec3]) {
let vertices : Array[@smath.Vec3] = []
let uvs : Array[@smath.Vec2] = []
let normals : Array[@smath.Vec3] = []
let segment_count = if slices < 3 { 3 } else { slices }
let half_h = height / 2.0
let slope_y = if height.abs() < 0.000001 {
0.0
} else {
(radius_bottom - radius_top) / height
}
for segment in 0.. 0.000001 {
let top_center = @smath.Vec3(0.0, half_h, 0.0)
let top_normal = @smath.Vec3(0.0, 1.0, 0.0)
push_textured_triangle(
vertices,
uvs,
normals,
top_center,
top1,
top0,
Vec2(0.5, 0.5),
Vec2(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
Vec2(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
top_normal,
top_normal,
top_normal,
)
}
if radius_bottom.abs() > 0.000001 {
let bottom_center = @smath.Vec3(0.0, -half_h, 0.0)
let bottom_normal = @smath.Vec3(0.0, -1.0, 0.0)
push_textured_triangle(
vertices,
uvs,
normals,
bottom_center,
bot0,
bot1,
Vec2(0.5, 0.5),
Vec2(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
Vec2(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
bottom_normal,
bottom_normal,
bottom_normal,
)
}
}
(vertices, uvs, normals)
}
///|
fn push_textured_triangle(
vertices : Array[@smath.Vec3],
uvs : Array[@smath.Vec2],
normals : Array[@smath.Vec3],
a : @smath.Vec3,
b : @smath.Vec3,
c : @smath.Vec3,
uv_a : @smath.Vec2,
uv_b : @smath.Vec2,
uv_c : @smath.Vec2,
n_a : @smath.Vec3,
n_b : @smath.Vec3,
n_c : @smath.Vec3,
) -> Unit {
vertices.push(a)
vertices.push(b)
vertices.push(c)
uvs.push(uv_a)
uvs.push(uv_b)
uvs.push(uv_c)
normals.push(n_a)
normals.push(n_b)
normals.push(n_c)
}