// 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.
///|
fn alpha_mode_to_code(alpha_mode : @render.AlphaMode2D) -> Int {
match alpha_mode {
Opaque => 0
Blend => 1
}
}
///|
fn blend_mode_to_code(blend_mode : @render.BlendMode2D) -> Int {
match blend_mode {
Alpha => 0
Additive => 1
Multiply => 2
}
}
///|
fn color_pipeline2d_code(
topology : String,
alpha_code : Int,
blend_code : Int,
) -> Int {
if topology == "line" {
match blend_code {
1 => 6
2 => 7
_ => if alpha_code == 0 { 5 } else { 4 }
}
} else {
match blend_code {
1 => 2
2 => 3
_ => if alpha_code == 0 { 1 } else { 0 }
}
}
}
///|
fn texture_pipeline2d_code(alpha_code : Int, blend_code : Int) -> Int {
match blend_code {
1 => 10
2 => 11
_ => if alpha_code == 0 { 9 } else { 8 }
}
}
///|
fn color_rgba(color : @render.Color) -> (Double, Double, Double, Double) {
(
color.r.reinterpret_as_int().to_double(),
color.g.reinterpret_as_int().to_double(),
color.b.reinterpret_as_int().to_double(),
color.a,
)
}
///|
const MAX_DIRECTIONAL_LIGHTS : Int = 4
///|
const MAX_POINT_LIGHTS : Int = 8
///|
const MAX_SPOT_LIGHTS : Int = 4
///|
const TEXTURED_SPHERE_SLICES : Int = 24
///|
const TEXTURED_SPHERE_STACKS : Int = 16
///|
const WEBGPU_PRIMITIVE_MESH_RESOURCE_TTL_FRAMES : Int = 120
///|
pub(all) enum Render3DLightingMode {
Full
Cheap
Unlit
} derive(Eq, Debug)
///|
let synced_mesh_assets : Map[
@render3d_types.MeshHandle,
@render3d_types.MeshAsset,
] = Map([])
///|
let synced_material_assets : Map[
@render3d_types.MaterialHandle,
@render3d_types.StandardMaterial3D,
] = Map([])
///|
let synced_image_assets : Map[
@render3d_types.ImageHandle,
@render3d_types.ImageAsset,
] = Map([])
///|
let synced_cubemap_assets : Map[
@render3d_types.CubemapHandle,
@render3d_types.CubemapAsset3D,
] = Map([])
///|
let textured_primitive_mesh_cache : Map[String, @render3d_types.TriangleMesh3D] = Map([],
)
///|
priv struct MaterialTextureSource3D {
path : String
texcoord_set : Int
sampler_code : Int
}
///|
priv struct MaterialTextureBundle3D {
base_source : MaterialTextureSource3D?
emissive_source : MaterialTextureSource3D?
metallic_roughness_source : MaterialTextureSource3D?
occlusion_source : MaterialTextureSource3D?
normal_source : MaterialTextureSource3D?
primary_texcoord_set : Int
}
///|
priv struct ResolvedRender3DMaterial {
material : @render3d_types.StandardMaterial3D
color : @render.Color
emissive_r : Double
emissive_g : Double
emissive_b : Double
alpha_mode : Double
alpha_cutoff : Double
unlit : Bool
texture_bundle : MaterialTextureBundle3D?
}
///|
priv struct Render3DSubmissionStats {
mut sections : Int
mut submitted_items : Int
mut skipped_items : Int
mut command_groups : Int
mut draw_commands : Int
mut lit_triangle_commands : Int
mut textured_triangle_commands : Int
mut uploaded_vertex_bytes : Int
mut render_bind_group_creations : Int
}
///|
pub(all) struct Render3DSubmissionStatsSnapshot {
sections : Int
submitted_items : Int
skipped_items : Int
command_groups : Int
draw_commands : Int
lit_triangle_commands : Int
textured_triangle_commands : Int
uploaded_vertex_bytes : Int
render_bind_group_creations : Int
} derive(Eq, Debug)
///|
pub(all) struct WebGpuRender3DTimingSnapshot {
setup_ms : Double
items_ms : Double
flush_ms : Double
cleanup_ms : Double
submit_total_ms : Double
} derive(Eq, Debug)
///|
priv struct LitPrimitiveMeshBatchAccumulator {
order : Array[Int]
indices : Map[LitPrimitiveMeshBatchKey, Int]
entries : Array[LitPrimitiveMeshBatch]
}
///|
priv struct LitPrimitiveMeshBatchKey {
mesh : @render3d_types.MeshHandle
material : @render3d_types.MaterialHandle
cast_shadows : Bool
receive_shadows : Bool
} derive(Eq, Hash)
///|
priv struct LitPrimitiveMeshBatch {
resource_key : String
instances : Array[Double]
double_sided : Bool
cast_shadows : Bool
receive_shadows : Bool
}
///|
let last_render3d_submission_stats : Ref[Render3DSubmissionStats] = Ref(
empty_render3d_submission_stats(),
)
///|
let last_render3d_submission_stats_checksum : Ref[Int] = Ref(0)
///|
let last_render3d_timing : Ref[WebGpuRender3DTimingSnapshot] = Ref({
setup_ms: 0.0,
items_ms: 0.0,
flush_ms: 0.0,
cleanup_ms: 0.0,
submit_total_ms: 0.0,
})
///|
let retained_render3d_instance_last_seen : Map[String, Int] = Map([])
///|
let current_render3d_lighting_mode : Ref[Render3DLightingMode] = Ref(Full)
///|
let webgpu_resource_frame : Ref[Int] = Ref(0)
///|
let primitive_mesh_resource_last_seen : Map[String, Int] = Map([])
///|
let uploaded_primitive_mesh_resources : Map[String, Bool] = Map([])
///|
fn empty_render3d_submission_stats() -> Render3DSubmissionStats {
{
sections: 0,
submitted_items: 0,
skipped_items: 0,
command_groups: 0,
draw_commands: 0,
lit_triangle_commands: 0,
textured_triangle_commands: 0,
uploaded_vertex_bytes: 0,
render_bind_group_creations: 0,
}
}
///|
fn render3d_submission_stats_snapshot(
stats : Render3DSubmissionStats,
) -> Render3DSubmissionStatsSnapshot {
{
sections: stats.sections,
submitted_items: stats.submitted_items,
skipped_items: stats.skipped_items,
command_groups: stats.command_groups,
draw_commands: stats.draw_commands,
lit_triangle_commands: stats.lit_triangle_commands,
textured_triangle_commands: stats.textured_triangle_commands,
uploaded_vertex_bytes: stats.uploaded_vertex_bytes,
render_bind_group_creations: stats.render_bind_group_creations,
}
}
///|
pub fn render3d_last_submission_stats() -> Render3DSubmissionStatsSnapshot {
render3d_submission_stats_snapshot(last_render3d_submission_stats.val)
}
///|
pub fn webgpu_last_render3d_timing() -> WebGpuRender3DTimingSnapshot {
{ ..last_render3d_timing.val }
}
///|
priv struct ResolvedMaterialTextureInput3D {
path : String
uvs : Array[@smath.Vec2]
sampler_code : Int
}
///|
pub fn preload_img(path : String) -> Unit {
webgpu_preload_img(resolve_asset_path(path))
}
///|
pub fn frame_begin(clear : @render.Color) -> Unit {
webgpu_begin_frame(
clear.r.reinterpret_as_int().to_double() / 255.0,
clear.g.reinterpret_as_int().to_double() / 255.0,
clear.b.reinterpret_as_int().to_double() / 255.0,
clear.a,
)
}
///|
pub fn frame_end() -> Unit {
webgpu_end_frame()
}
///|
pub fn set_render3d_lighting_mode(mode : Render3DLightingMode) -> Unit {
current_render3d_lighting_mode.val = mode
}
///|
pub fn capture_frame_png() -> Bytes? {
let raw = webgpu_capture_frame_png_bytes()
if raw.length() == 0 {
return None
}
Some(
Bytes::from_iter(raw.iter().map(fn(value) { saturating_file_byte(value) })),
)
}
///|
pub fn capture_frame_png_to_file(_path : String) -> Bool {
false
}
///|
pub fn render2d_begin_pass(
viewport : @smath.Rect,
load_op : @render2d_types.RenderPassLoadOp2D,
clear_color : @render.Color,
target : @render2d_types.RenderPassTarget2D,
) -> Unit {
let target_name = match target {
Screen => ""
Offscreen(name) => name
}
let (cr, cg, cb, ca) = color_rgba(clear_color)
webgpu_begin_2d_pass(
viewport.position[X],
viewport.position[Y],
viewport.size[X],
viewport.size[Y],
if load_op is Clear {
1
} else {
0
},
cr,
cg,
cb,
ca,
target_name,
)
if load_op is Clear {
let clear_viewport = match target {
Screen => viewport
Offscreen(_) => { position: @smath.Vec2::zero(), size: viewport.size }
}
webgpu_push_color_vertices_2d(
pack_rect_fill_vertices2d(clear_viewport, Transform(), clear_color),
color_pipeline2d_code(
"triangle",
if clear_color.a >= 0.999999 {
0
} else {
1
},
0,
),
)
}
}
///|
pub fn render2d_end_pass() -> Unit {
webgpu_end_2d_pass()
}
///|
pub fn push_clip_rect(rect : @smath.Rect) -> Unit {
webgpu_push_clip_rect(
rect.position[X],
rect.position[Y],
rect.size[X],
rect.size[Y],
)
}
///|
pub fn pop_clip_rect() -> Unit {
webgpu_pop_clip_rect()
}
///|
pub fn reset_clip_stack() -> Unit {
webgpu_reset_clip_stack()
}
///|
pub fn draw_image(
command : @render2d_types.ImageDrawCommand2D,
image_path : String,
sampler : @asset_types.ImageSampler,
) -> Unit {
let path = resolve_asset_path(image_path)
let dimensions = webgpu_image_dimensions(path)
if dimensions.length() < 3 ||
dimensions[0] < 0.5 ||
dimensions[1] <= 0.0 ||
dimensions[2] <= 0.0 {
return
}
let packed = pack_image_vertices2d(command, dimensions[1], dimensions[2])
webgpu_push_texture_vertices_2d(
path,
packed.sampler_code,
match sampler {
Default => 0
Nearest => 1
Linear => 2
},
packed.vertices,
texture_pipeline2d_code(
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
}
///|
pub fn draw_image_material(
command : @render2d_types.ImageMaterialDrawCommand2D,
image_path : String,
sampler : @asset_types.ImageSampler,
) -> Unit {
let path = resolve_asset_path(image_path)
let dimensions = webgpu_image_dimensions(path)
if dimensions.length() < 3 ||
dimensions[0] < 0.5 ||
dimensions[1] <= 0.0 ||
dimensions[2] <= 0.0 {
return
}
webgpu_push_image_material_vertices_2d(
path,
match sampler {
Default => 0
Nearest => 1
Linear => 2
},
pack_image_material_vertices2d(command, dimensions[1], dimensions[2]),
)
}
///|
pub fn draw_text(command : @render2d_types.TextDrawCommand2D) -> Unit {
if draw_text_with_cosmic(command) {
return
}
let (r, g, b, a) = color_rgba(command.style.color)
let dimensions = webgpu_text_texture_dimensions(
command.text,
command.style.family,
command.style.size,
command.style.weight.value().reinterpret_as_int(),
r,
g,
b,
a,
)
if dimensions.length() < 3 ||
dimensions[0] < 0.5 ||
dimensions[1] <= 0.0 ||
dimensions[2] <= 0.0 {
return
}
webgpu_push_text_vertices_2d(
command.text,
command.style.family,
command.style.size,
command.style.weight.value().reinterpret_as_int(),
r,
g,
b,
a,
pack_text_vertices2d(command, dimensions[1], dimensions[2]),
texture_pipeline2d_code(1, blend_mode_to_code(command.blend_mode)),
)
}
///|
pub fn measure_text(
style : @render2d_types.TextStyle2D,
text : String,
) -> @smath.Vec2 {
let measured = webgpu_measure_text(
text,
style.family,
style.size,
style.weight.value().reinterpret_as_int(),
)
if measured.length() >= 2 {
Vec2(measured[0], measured[1])
} else {
let width = text.length().to_double() * style.size * 0.6
let height = if style.size <= 0.0 { 1.0 } else { style.size }
Vec2(width, height)
}
}
///|
pub fn measure_text_block(
command : @render2d_types.TextDrawCommand2D,
) -> @smath.Vec2 {
let mut fonts_ready = ensure_font_loaded(command.style.family)
for section in command.sections {
fonts_ready = ensure_font_loaded(section.style.family) && fonts_ready
}
if fonts_ready {
return @text_pipeline.compute_text_block(
command,
cosmic_font_system.val,
1.0,
).size()
}
measure_text(command.style, command.text)
}
///|
pub fn draw_rect(command : @render2d_types.RectDrawCommand2D) -> Unit {
webgpu_push_color_vertices_2d(
pack_rect_fill_vertices2d(
command.rect,
command.transform,
command.fill_color,
),
color_pipeline2d_code(
"triangle",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
match command.stroke_color {
Some(stroke_color) =>
webgpu_push_color_vertices_2d(
pack_rect_stroke_vertices2d(
command.rect,
command.transform,
stroke_color,
),
color_pipeline2d_code(
"line",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
None => ()
}
}
///|
pub fn draw_circle(command : @render2d_types.CircleDrawCommand2D) -> Unit {
webgpu_push_color_vertices_2d(
pack_circle_fill_vertices2d(
command.center,
command.radius,
command.transform,
command.fill_color,
),
color_pipeline2d_code(
"triangle",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
match command.stroke_color {
Some(stroke_color) =>
webgpu_push_color_vertices_2d(
pack_circle_stroke_vertices2d(
command.center,
command.radius,
command.transform,
stroke_color,
),
color_pipeline2d_code(
"line",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
None => ()
}
}
///|
pub fn draw_geometry(command : @render2d_types.GeometryDrawCommand2D) -> Unit {
let primitive = match command.topology {
TriangleList => "triangle"
LineList => "line"
}
webgpu_push_color_vertices_2d(
pack_geometry_vertices2d(command),
color_pipeline2d_code(
primitive,
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
}
///|
pub fn draw_gradient_rect(
command : @render2d_types.GradientRectDrawCommand2D,
) -> Unit {
webgpu_push_color_vertices_2d(
pack_gradient_rect_vertices2d(command),
color_pipeline2d_code(
"triangle",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
}
///|
pub fn draw_rounded_chrome(
command : @render2d_types.RoundedChromeDrawCommand2D,
) -> Unit {
let cached = cached_rounded_chrome_vertices2d(command)
webgpu_push_cached_color_vertices_2d(
cached.cache_id,
cached.vertices,
color_pipeline2d_code(
"triangle",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
}
///|
pub fn draw_colored_geometry(
command : @render2d_types.ColoredGeometryDrawCommand2D,
) -> Unit {
webgpu_push_color_vertices_2d(
pack_colored_geometry_vertices2d(command),
color_pipeline2d_code(
"triangle",
alpha_mode_to_code(command.alpha_mode),
blend_mode_to_code(command.blend_mode),
),
)
}
///|
fn begin_3d(camera : @render3d_types.FrameCamera3D, aspect : Double) -> Unit {
webgpu_begin_3d(
pack_camera_view_projection3d(camera, aspect),
camera.position.x,
camera.position.y,
camera.position.z,
camera.target.x,
camera.target.y,
camera.target.z,
camera.up.x,
camera.up.y,
camera.up.z,
camera.fov_y,
camera.near,
camera.far,
if camera.projection == Orthographic {
1.0
} else {
0.0
},
camera.orthographic_size.map_or(0.0, fn(size) { size[X] }),
camera.orthographic_size.map_or(0.0, fn(size) { size[Y] }),
)
}
///|
fn end_3d() -> Unit {
webgpu_end_3d()
}
///|
fn draw_render3d_lines(frame : @render3d_types.RenderFrame3D) -> Int {
guard frame.lines.length() > 0 else { return 0 }
let vertices : Array[Double] = []
for line in frame.lines {
push_line3d_vertex(vertices, line.start, line.color)
push_line3d_vertex(vertices, line.end, line.color)
}
webgpu_draw_lines_3d(vertices)
1
}
///|
fn push_line3d_vertex(
vertices : Array[Double],
position : @smath.Vec3,
color : @render.Color,
) -> Unit {
let (r, g, b, a) = color_rgba01(color)
vertices.push(position.x)
vertices.push(position.y)
vertices.push(position.z)
vertices.push(r)
vertices.push(g)
vertices.push(b)
vertices.push(a)
}
///|
pub fn sync_mesh_asset(
handle : @render3d_types.MeshHandle,
mesh : @render3d_types.MeshAsset,
) -> Unit {
synced_mesh_assets.set(handle, mesh)
}
///|
pub fn sync_material_asset(
handle : @render3d_types.MaterialHandle,
material : @render3d_types.StandardMaterial3D,
) -> Unit {
synced_material_assets.set(handle, material)
}
///|
pub fn sync_image_asset(
handle : @render3d_types.ImageHandle,
image : @render3d_types.ImageAsset,
) -> Unit {
synced_image_assets.set(handle, image)
}
///|
pub fn sync_cubemap_asset(
handle : @render3d_types.CubemapHandle,
cubemap : @render3d_types.CubemapAsset3D,
) -> Unit {
synced_cubemap_assets.set(handle, cubemap)
}
///|
pub fn release_mesh_asset(handle : @render3d_types.MeshHandle) -> Unit {
synced_mesh_assets.remove(handle)
}
///|
pub fn release_material_asset(handle : @render3d_types.MaterialHandle) -> Unit {
synced_material_assets.remove(handle)
}
///|
pub fn release_image_asset(handle : @render3d_types.ImageHandle) -> Unit {
synced_image_assets.remove(handle)
}
///|
pub fn release_cubemap_asset(handle : @render3d_types.CubemapHandle) -> Unit {
synced_cubemap_assets.remove(handle)
}
///|
fn draw_skybox3d(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
) -> Unit {
guard frame.skybox is Some(skybox) else { return }
guard synced_cubemap_assets.get(skybox.cubemap) is Some(cubemap) else {
return
}
match cubemap {
SixFaces(faces) => {
let half_size = skybox_half_size(camera)
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),
)
}
}
}
///|
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 max_axis = if size[X].abs() > size[Y].abs() {
size[X].abs()
} else {
size[Y].abs()
}
let cover_half = max_axis * 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 synced_image_assets.get(image) is Some(image_asset) else { return }
let path = resolve_asset_path(image_asset.path)
let vertices : Array[Double] = []
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p0, 0.0, 1.0, brightness,
)
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p1, 1.0, 1.0, brightness,
)
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p2, 1.0, 0.0, brightness,
)
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p0, 0.0, 1.0, brightness,
)
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p2, 1.0, 0.0, brightness,
)
push_skybox_vertex(
vertices, camera_position, rotation, half_size, p3, 0.0, 0.0, brightness,
)
webgpu_draw_skybox_textured_triangles_3d(path, vertices)
}
///|
fn push_skybox_vertex(
vertices : Array[Double],
camera_position : @smath.Vec3,
rotation : @smath.Quat,
half_size : Double,
direction : @smath.Vec3,
u : Double,
v : Double,
brightness : Double,
) -> Unit {
let position = camera_position +
rotation.rotate_vec3(direction.scalar_mul(half_size))
let color = if brightness < 0.0 { 0.0 } else { brightness }
vertices.push(position.x)
vertices.push(position.y)
vertices.push(position.z)
vertices.push(direction.x)
vertices.push(direction.y)
vertices.push(direction.z)
vertices.push(u)
vertices.push(v)
vertices.push(u)
vertices.push(v)
vertices.push(u)
vertices.push(v)
vertices.push(u)
vertices.push(v)
vertices.push(u)
vertices.push(v)
vertices.push(color)
vertices.push(color)
vertices.push(color)
vertices.push(1.0)
vertices.push(0.0)
vertices.push(0.0)
vertices.push(0.0)
vertices.push(1.0)
vertices.push(0.0)
vertices.push(0.0)
vertices.push(1.0)
vertices.push(0.0)
vertices.push(0.0)
vertices.push(0.0)
vertices.push(1.0)
vertices.push(1.0)
vertices.push(0.0)
vertices.push(1.0)
vertices.push(0.0)
}
///|
fn resolve_render3d_material(
cache : Map[@render3d_types.MaterialHandle, ResolvedRender3DMaterial],
handle : @render3d_types.MaterialHandle,
) -> ResolvedRender3DMaterial {
match cache.get(handle) {
Some(resolved) => resolved
None => {
let material = synced_material_assets
.get(handle)
.unwrap_or(@render3d_types.default_standard_material3d())
let color = effective_material_color(material)
let (emissive_r, emissive_g, emissive_b) = color_rgb01(
material.emissive_color,
)
let resolved = {
material,
color,
emissive_r,
emissive_g,
emissive_b,
alpha_mode: alpha_mode_code(material.alpha_mode),
alpha_cutoff: material.alpha_cutoff,
unlit: material.unlit,
texture_bundle: resolve_material_texture_bundle(material),
}
cache.set(handle, resolved)
resolved
}
}
}
///|
pub fn render3d_submit(frame : @render3d_types.RenderFrame3D) -> Unit {
let submit_start = webgpu_now_ms()
begin_webgpu_resource_frame()
let stats = empty_render3d_submission_stats()
guard frame.camera is Some(active_camera) else {
last_render3d_submission_stats.val = stats
last_render3d_submission_stats_checksum.val = render3d_submission_stats_checksum(
stats,
)
prune_stale_primitive_mesh_resources()
prune_stale_retained_instance_buffers3d()
let submit_end = webgpu_now_ms()
last_render3d_timing.val = {
setup_ms: 0.0,
items_ms: 0.0,
flush_ms: 0.0,
cleanup_ms: submit_end - submit_start,
submit_total_ms: submit_end - submit_start,
}
return
}
stats.sections = 1
let setup_start = webgpu_now_ms()
let aspect = webgpu_canvas_aspect_ratio()
configure_scene_lighting(frame, active_camera, aspect)
begin_3d(active_camera, aspect)
draw_skybox3d(frame, active_camera)
let lit_primitive_batches = new_lit_primitive_mesh_batch_accumulator()
let material_cache : Map[
@render3d_types.MaterialHandle,
ResolvedRender3DMaterial,
] = Map([])
let item_start = webgpu_now_ms()
for item in frame.items {
guard synced_mesh_assets.get(item.mesh) is Some(mesh_asset) else {
stats.skipped_items += 1
continue
}
let resolved_material = resolve_render3d_material(
material_cache,
item.material,
)
let material = resolved_material.material
let vertex_count = render3d_item_vertex_count(mesh_asset.primitive)
if vertex_count <= 0 {
stats.skipped_items += 1
continue
}
stats.submitted_items += 1
let center = item.transform.translation
let rotation = item.transform.rotation
let color = resolved_material.color
let emissive_r = resolved_material.emissive_r
let emissive_g = resolved_material.emissive_g
let emissive_b = resolved_material.emissive_b
let alpha_mode = resolved_material.alpha_mode
let alpha_cutoff = resolved_material.alpha_cutoff
let unlit = resolved_material.unlit
let texture_bundle = resolved_material.texture_bundle
match mesh_asset.primitive {
Cube(size) =>
match texture_bundle {
Some(bundle) => {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
draw_triangle_mesh_instance(
cached_cuboid_triangle_mesh3d(
size.x,
size.y,
size.z,
required_primitive_uv_set_count(bundle),
),
material,
Some(bundle),
color,
center,
rotation,
item.transform.scale,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
None => {
let primitive_mesh = cached_cuboid_triangle_mesh3d(
size.x,
size.y,
size.z,
1,
)
enqueue_lit_primitive_mesh_batch(
lit_primitive_batches,
lit_primitive_mesh_batch_key(item),
fn() {
ensure_lit_primitive_mesh_resource(
lit_primitive_mesh_resource_key(
"lit-cube",
[size.x, size.y, size.z],
color,
emissive_r,
emissive_g,
emissive_b,
alpha_mode,
alpha_cutoff,
unlit,
),
primitive_mesh,
material,
color,
item.receive_shadows,
)
},
center.x,
center.y,
center.z,
rotation.x,
rotation.y,
rotation.z,
rotation.w,
item.transform.scale.x,
item.transform.scale.y,
item.transform.scale.z,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
}
Sphere(radius) =>
match texture_bundle {
Some(bundle) => {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
draw_triangle_mesh_instance(
cached_sphere_triangle_mesh3d(
radius,
TEXTURED_SPHERE_SLICES,
TEXTURED_SPHERE_STACKS,
required_primitive_uv_set_count(bundle),
),
material,
Some(bundle),
color,
center,
rotation,
item.transform.scale,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
None => {
let primitive_mesh = cached_sphere_triangle_mesh3d(
radius,
TEXTURED_SPHERE_SLICES,
TEXTURED_SPHERE_STACKS,
1,
)
enqueue_lit_primitive_mesh_batch(
lit_primitive_batches,
lit_primitive_mesh_batch_key(item),
fn() {
ensure_lit_primitive_mesh_resource(
lit_primitive_mesh_resource_key(
"lit-sphere",
[radius],
color,
emissive_r,
emissive_g,
emissive_b,
alpha_mode,
alpha_cutoff,
unlit,
),
primitive_mesh,
material,
color,
item.receive_shadows,
)
},
center.x,
center.y,
center.z,
rotation.x,
rotation.y,
rotation.z,
rotation.w,
item.transform.scale.x,
item.transform.scale.y,
item.transform.scale.z,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
}
Cylinder(radius_top, radius_bottom, height, slices) =>
match texture_bundle {
Some(bundle) => {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
draw_triangle_mesh_instance(
cached_cylinder_triangle_mesh3d(
radius_top,
radius_bottom,
height,
slices,
required_primitive_uv_set_count(bundle),
),
material,
Some(bundle),
color,
center,
rotation,
item.transform.scale,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
None => {
let primitive_mesh = cached_cylinder_triangle_mesh3d(
radius_top, radius_bottom, height, slices, 1,
)
enqueue_lit_primitive_mesh_batch(
lit_primitive_batches,
lit_primitive_mesh_batch_key(item),
fn() {
ensure_lit_primitive_mesh_resource(
lit_primitive_mesh_resource_key(
"lit-cylinder",
[
radius_top,
radius_bottom,
height,
safe_cylinder_slices(slices).to_double(),
],
color,
emissive_r,
emissive_g,
emissive_b,
alpha_mode,
alpha_cutoff,
unlit,
),
primitive_mesh,
material,
color,
item.receive_shadows,
)
},
center.x,
center.y,
center.z,
rotation.x,
rotation.y,
rotation.z,
rotation.w,
item.transform.scale.x,
item.transform.scale.y,
item.transform.scale.z,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
}
Plane(size) => {
let y = if size.y.abs() < 0.0001 { 0.02 } else { size.y }
match texture_bundle {
Some(bundle) => {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
draw_triangle_mesh_instance(
cached_cuboid_triangle_mesh3d(
size.x,
y,
size.z,
required_primitive_uv_set_count(bundle),
),
material,
Some(bundle),
color,
center,
rotation,
item.transform.scale,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
None => {
let primitive_mesh = cached_cuboid_triangle_mesh3d(
size.x,
y,
size.z,
1,
)
enqueue_lit_primitive_mesh_batch(
lit_primitive_batches,
lit_primitive_mesh_batch_key(item),
fn() {
ensure_lit_primitive_mesh_resource(
lit_primitive_mesh_resource_key(
"lit-cube",
[size.x, y, size.z],
color,
emissive_r,
emissive_g,
emissive_b,
alpha_mode,
alpha_cutoff,
unlit,
),
primitive_mesh,
material,
color,
item.receive_shadows,
)
},
center.x,
center.y,
center.z,
rotation.x,
rotation.y,
rotation.z,
rotation.w,
item.transform.scale.x,
item.transform.scale.y,
item.transform.scale.z,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
}
}
Triangles(mesh) => {
if texture_bundle is Some(_) &&
render3d_item_can_use_textured_path(
mesh_asset.primitive,
texture_bundle,
) {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
} else {
stats.lit_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 17 * 4
}
draw_triangle_mesh_instance(
mesh,
material,
texture_bundle,
color,
center,
rotation,
item.transform.scale,
material.double_sided,
item.cast_shadows,
item.receive_shadows,
)
}
}
}
let line_commands = draw_render3d_lines(frame)
if line_commands > 0 {
stats.command_groups += 1
stats.uploaded_vertex_bytes += frame.lines.length() * 2 * 7 * 4
}
let flush_start = webgpu_now_ms()
flush_lit_primitive_mesh_batches(lit_primitive_batches, stats)
if stats.lit_triangle_commands > 0 {
stats.command_groups += 1
stats.render_bind_group_creations += 1
}
if stats.textured_triangle_commands > 0 {
stats.command_groups += 1
stats.render_bind_group_creations += stats.textured_triangle_commands
}
stats.draw_commands = stats.lit_triangle_commands +
stats.textured_triangle_commands +
line_commands
last_render3d_submission_stats.val = stats
last_render3d_submission_stats_checksum.val = render3d_submission_stats_checksum(
stats,
)
let cleanup_start = webgpu_now_ms()
end_3d()
prune_stale_primitive_mesh_resources()
prune_stale_retained_instance_buffers3d()
let submit_end = webgpu_now_ms()
last_render3d_timing.val = {
setup_ms: item_start - setup_start,
items_ms: flush_start - item_start,
flush_ms: cleanup_start - flush_start,
cleanup_ms: submit_end - cleanup_start,
submit_total_ms: submit_end - submit_start,
}
}
///|
fn render3d_submission_stats_checksum(stats : Render3DSubmissionStats) -> Int {
stats.sections +
stats.submitted_items +
stats.skipped_items +
stats.command_groups +
stats.draw_commands +
stats.lit_triangle_commands +
stats.textured_triangle_commands +
stats.uploaded_vertex_bytes +
stats.render_bind_group_creations
}
///|
fn begin_webgpu_resource_frame() -> Unit {
webgpu_resource_frame.val += 1
}
///|
fn new_lit_primitive_mesh_batch_accumulator() -> LitPrimitiveMeshBatchAccumulator {
{ order: [], indices: Map([]), entries: [] }
}
///|
fn lit_primitive_mesh_batch_key(
item : @render3d_types.RenderItem3D,
) -> LitPrimitiveMeshBatchKey {
{
mesh: item.mesh,
material: item.material,
cast_shadows: item.cast_shadows,
receive_shadows: item.receive_shadows,
}
}
///|
fn enqueue_lit_primitive_mesh_batch(
batches : LitPrimitiveMeshBatchAccumulator,
key : LitPrimitiveMeshBatchKey,
build_resource_key : () -> String,
center_x : Double,
center_y : Double,
center_z : Double,
rotation_x : Double,
rotation_y : Double,
rotation_z : Double,
rotation_w : Double,
scale_x : Double,
scale_y : Double,
scale_z : Double,
double_sided : Bool,
cast_shadows : Bool,
receive_shadows : Bool,
) -> Unit {
let batch_index = match batches.indices.get(key) {
Some(index) => index
None => {
let index = batches.entries.length()
batches.order.push(index)
batches.indices.set(key, index)
batches.entries.push({
resource_key: build_resource_key(),
instances: [],
double_sided,
cast_shadows,
receive_shadows,
})
index
}
}
let entry = batches.entries[batch_index]
push_lit_primitive_instance_values(
entry.instances,
center_x,
center_y,
center_z,
rotation_x,
rotation_y,
rotation_z,
rotation_w,
scale_x,
scale_y,
scale_z,
receive_shadows,
)
}
///|
fn flush_lit_primitive_mesh_batches(
batches : LitPrimitiveMeshBatchAccumulator,
stats : Render3DSubmissionStats,
) -> Unit {
for index in batches.order {
let entry = batches.entries[index]
let instances = entry.instances
if instances.length() == 0 {
continue
}
let retained_key = retained_lit_primitive_instance_key(
lit_primitive_instance_batch_key(
entry.resource_key,
entry.double_sided,
entry.cast_shadows,
entry.receive_shadows,
),
instances,
)
if !retained_render3d_instance_last_seen.contains(retained_key) {
stats.uploaded_vertex_bytes += instances.length() * 4
}
ignore(mark_retained_instance_buffer_key(retained_key))
stats.lit_triangle_commands += 1
webgpu_draw_lit_primitive_mesh_batch_3d(
entry.resource_key,
retained_key,
instances,
entry.double_sided,
entry.cast_shadows,
)
}
}
///|
fn lit_primitive_instance_batch_key(
resource_key : String,
double_sided : Bool,
cast_shadows : Bool,
receive_shadows : Bool,
) -> String {
resource_key +
"|" +
(if double_sided { "double" } else { "single" }) +
"|" +
(if cast_shadows { "cast" } else { "no-cast" }) +
"|" +
(if receive_shadows { "receive" } else { "no-receive" })
}
///|
fn push_lit_primitive_instance_values(
instances : Array[Double],
center_x : Double,
center_y : Double,
center_z : Double,
rotation_x : Double,
rotation_y : Double,
rotation_z : Double,
rotation_w : Double,
scale_x : Double,
scale_y : Double,
scale_z : Double,
receive_shadows : Bool,
) -> Unit {
instances.push(center_x)
instances.push(center_y)
instances.push(center_z)
instances.push(rotation_x)
instances.push(rotation_y)
instances.push(rotation_z)
instances.push(rotation_w)
instances.push(scale_x)
instances.push(scale_y)
instances.push(scale_z)
instances.push(if receive_shadows { 1.0 } else { 0.0 })
}
///|
fn retained_lit_primitive_instance_key(
batch_key : String,
instances : Array[Double],
) -> String {
let mut hash_a = 17
let mut hash_b = 29
for value in instances {
let component = retained_instance_hash_component(value)
hash_a = (hash_a * 131 + component) % 1000003
hash_b = (hash_b * 257 + component) % 1000033
}
batch_key +
"|instances:" +
instances.length().to_string() +
":" +
hash_a.to_string() +
":" +
hash_b.to_string()
}
///|
fn retained_instance_hash_component(value : Double) -> Int {
let scaled = (value * 1000000.0).to_int()
let positive = if scaled < 0 { -scaled + 17 } else { scaled }
positive % 1000003
}
///|
fn mark_primitive_mesh_resource_key(key : String) -> String {
primitive_mesh_resource_last_seen.set(key, webgpu_resource_frame.val)
key
}
///|
fn ensure_lit_primitive_mesh_resource(
key : String,
mesh : @render3d_types.TriangleMesh3D,
material : @render3d_types.StandardMaterial3D,
color : @render.Color,
receive_shadows : Bool,
) -> String {
let marked_key = mark_primitive_mesh_resource_key(key)
if !uploaded_primitive_mesh_resources.contains(marked_key) {
let vertices = flatten_colored_vertices3d(
mesh.positions,
resolve_triangle_indices(mesh).unwrap_or([]),
resolve_mesh_normals(mesh),
mesh.colors,
@smath.Vec3::one(),
material,
color,
receive_shadows,
)
webgpu_upload_lit_primitive_mesh_resource(marked_key, vertices)
uploaded_primitive_mesh_resources.set(marked_key, true)
}
marked_key
}
///|
fn prune_stale_primitive_mesh_resources() -> Unit {
let min_live_frame = webgpu_resource_frame.val -
WEBGPU_PRIMITIVE_MESH_RESOURCE_TTL_FRAMES
for pair in primitive_mesh_resource_last_seen.to_array() {
if pair.1 < min_live_frame {
ignore(primitive_mesh_resource_last_seen.remove(pair.0))
ignore(uploaded_primitive_mesh_resources.remove(pair.0))
webgpu_release_primitive_mesh_resource(pair.0)
}
}
}
///|
fn mark_retained_instance_buffer_key(key : String) -> String {
retained_render3d_instance_last_seen.set(key, webgpu_resource_frame.val)
key
}
///|
fn prune_stale_retained_instance_buffers3d() -> Unit {
let min_live_frame = webgpu_resource_frame.val -
WEBGPU_PRIMITIVE_MESH_RESOURCE_TTL_FRAMES
for pair in retained_render3d_instance_last_seen.to_array() {
if pair.1 < min_live_frame {
ignore(retained_render3d_instance_last_seen.remove(pair.0))
webgpu_release_retained_instance_buffer_3d(pair.0)
}
}
}
///|
fn lit_primitive_mesh_resource_key(
prefix : String,
dimensions : Array[Double],
color : @render.Color,
emissive_r : Double,
emissive_g : Double,
emissive_b : Double,
alpha_mode : Double,
alpha_cutoff : Double,
unlit : Bool,
) -> String {
let mut key = prefix
for dimension in dimensions {
key = key + "|" + webgpu_resource_float_key(dimension)
}
key +
"|" +
webgpu_resource_color_key(color) +
"|" +
webgpu_resource_float_key(emissive_r) +
"," +
webgpu_resource_float_key(emissive_g) +
"," +
webgpu_resource_float_key(emissive_b) +
"|" +
webgpu_resource_float_key(alpha_mode) +
"|" +
webgpu_resource_float_key(alpha_cutoff) +
"|" +
(if unlit { "1" } else { "0" })
}
///|
fn webgpu_resource_color_key(color : @render.Color) -> String {
color.r.to_string() +
"," +
color.g.to_string() +
"," +
color.b.to_string() +
"," +
webgpu_resource_float_key(color.a)
}
///|
fn webgpu_resource_float_key(value : Double) -> String {
value.to_string()
}
///|
fn safe_cylinder_slices(slices : Int) -> Int {
if slices < 3 {
3
} else {
slices
}
}
///|
fn _collect_render3d_submission_stats(
frame : @render3d_types.RenderFrame3D,
) -> Render3DSubmissionStats {
let stats = empty_render3d_submission_stats()
let instanced_lit_buckets : Map[String, Bool] = Map([])
let instanced_lit_instances : Map[String, Array[Double]] = Map([])
let instanced_lit_counts : Map[String, Int] = Map([])
guard frame.camera is Some(_) else { return stats }
stats.sections = 1
for item in frame.items {
guard synced_mesh_assets.get(item.mesh) is Some(mesh_asset) else {
stats.skipped_items += 1
continue
}
let material = synced_material_assets
.get(item.material)
.unwrap_or(@render3d_types.default_standard_material3d())
let texture_bundle = resolve_material_texture_bundle(material)
let vertex_count = render3d_item_vertex_count(mesh_asset.primitive)
if vertex_count <= 0 {
stats.skipped_items += 1
continue
}
stats.submitted_items += 1
let textured = texture_bundle is Some(_) &&
render3d_item_can_use_textured_path(mesh_asset.primitive, texture_bundle)
if textured {
stats.textured_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 35 * 4
} else if render3d_item_can_use_instanced_lit_path(mesh_asset.primitive) {
let key = render3d_lit_instance_bucket_key(item)
if !instanced_lit_buckets.contains(key) {
instanced_lit_buckets.set(key, true)
stats.lit_triangle_commands += 1
}
instanced_lit_counts.set(
key,
instanced_lit_counts.get(key).unwrap_or(0) + 1,
)
push_render3d_item_instance_values(
instanced_lit_instances.get_or_init(key, fn() { [] }),
item,
)
} else {
stats.lit_triangle_commands += 1
stats.uploaded_vertex_bytes += vertex_count * 17 * 4
}
}
for key, instances in instanced_lit_instances {
let signature = retained_lit_primitive_instance_key(key, instances)
if !retained_render3d_instance_last_seen.contains(signature) {
stats.uploaded_vertex_bytes += instanced_lit_counts.get(key).unwrap_or(0) *
11 *
4
}
ignore(mark_retained_instance_buffer_key(signature))
}
if stats.lit_triangle_commands > 0 {
stats.command_groups += 1
stats.render_bind_group_creations += 1
}
if stats.textured_triangle_commands > 0 {
stats.command_groups += 1
stats.render_bind_group_creations += stats.textured_triangle_commands
}
if frame.lines.length() > 0 {
stats.command_groups += 1
stats.uploaded_vertex_bytes += frame.lines.length() * 2 * 7 * 4
}
let line_commands = if frame.lines.length() > 0 { 1 } else { 0 }
stats.draw_commands = stats.lit_triangle_commands +
stats.textured_triangle_commands +
line_commands
stats
}
///|
fn render3d_item_can_use_instanced_lit_path(
primitive : @render3d_types.MeshPrimitive3D,
) -> Bool {
match primitive {
Cube(_) | Sphere(_) | Cylinder(_, _, _, _) | Plane(_) => true
Triangles(_) => false
}
}
///|
fn render3d_lit_instance_bucket_key(
item : @render3d_types.RenderItem3D,
) -> String {
item.mesh.0.to_string() +
"|" +
item.material.0.to_string() +
"|" +
(if item.cast_shadows { "cast" } else { "no-cast" }) +
"|" +
(if item.receive_shadows { "receive" } else { "no-receive" })
}
///|
fn push_render3d_item_instance_values(
instances : Array[Double],
item : @render3d_types.RenderItem3D,
) -> Unit {
let transform = item.transform
let translation = transform.translation
let rotation = transform.rotation
let scale = transform.scale
push_lit_primitive_instance_values(
instances,
translation.x,
translation.y,
translation.z,
rotation.x,
rotation.y,
rotation.z,
rotation.w,
scale.x,
scale.y,
scale.z,
item.receive_shadows,
)
}
///|
fn render3d_item_can_use_textured_path(
primitive : @render3d_types.MeshPrimitive3D,
texture_bundle : MaterialTextureBundle3D?,
) -> Bool {
guard texture_bundle is Some(bundle) else { return false }
match primitive {
Cube(_) | Sphere(_) | Cylinder(_, _, _, _) | Plane(_) => true
Triangles(mesh) => textured_triangle_mesh_vertex_count(mesh, bundle) > 0
}
}
///|
fn render3d_item_vertex_count(
primitive : @render3d_types.MeshPrimitive3D,
) -> Int {
match primitive {
Cube(_) => cuboid_triangle_vertex_count()
Sphere(_) =>
sphere_triangle_vertex_count(
TEXTURED_SPHERE_SLICES,
TEXTURED_SPHERE_STACKS,
)
Cylinder(radius_top, radius_bottom, _, slices) =>
cylinder_triangle_vertex_count(radius_top, radius_bottom, slices)
Plane(_) => cuboid_triangle_vertex_count()
Triangles(mesh) =>
resolve_triangle_indices(mesh).map_or(0, fn(indices) { indices.length() })
}
}
///|
fn cuboid_triangle_vertex_count() -> Int {
36
}
///|
fn sphere_triangle_vertex_count(slices : Int, stacks : Int) -> Int {
let safe_slices = if slices < 3 { 3 } else { slices }
let safe_stacks = if stacks < 2 { 2 } else { stacks }
safe_slices * (safe_stacks - 1) * 6
}
///|
fn cylinder_triangle_vertex_count(
radius_top : Double,
radius_bottom : Double,
slices : Int,
) -> Int {
let safe_slices = if slices < 3 { 3 } else { slices }
let cap_vertices = (if radius_top.abs() > 0.000001 { 3 } else { 0 }) +
(if radius_bottom.abs() > 0.000001 { 3 } else { 0 })
safe_slices * (6 + cap_vertices)
}
///|
fn textured_triangle_mesh_vertex_count(
mesh : @render3d_types.TriangleMesh3D,
bundle : MaterialTextureBundle3D,
) -> Int {
guard resolve_triangle_indices(mesh) is Some(triangle_indices) else {
return 0
}
let normals = resolve_mesh_normals(mesh)
match mesh.uv_sets.get(bundle.primary_texcoord_set) {
Some(primary_uvs) if primary_uvs.length() == mesh.positions.length() &&
normals.length() == mesh.positions.length() => triangle_indices.length()
_ => 0
}
}
///|
fn draw_triangle_mesh_instance(
mesh : @render3d_types.TriangleMesh3D,
material : @render3d_types.StandardMaterial3D,
texture_bundle : MaterialTextureBundle3D?,
color : @render.Color,
translation : @smath.Vec3,
rotation : @smath.Quat,
scale : @smath.Vec3,
double_sided : Bool,
cast_shadows : Bool,
receive_shadows : Bool,
) -> Unit {
if resolve_triangle_indices(mesh) is Some(triangle_indices) &&
triangle_indices.length() >= 3 {
let normals = resolve_mesh_normals(mesh)
let tangents = resolve_mesh_tangents(mesh)
let transformed_positions = transform_positions3d(
mesh.positions,
translation,
rotation,
scale,
)
let transformed_normals = rotate_normals3d(normals, rotation, scale)
let draw_colored = fn() {
webgpu_draw_colored_triangles_3d(
flatten_colored_vertices3d(
transformed_positions,
triangle_indices,
transformed_normals,
mesh.colors,
@smath.Vec3::one(),
material,
color,
receive_shadows,
),
double_sided,
cast_shadows,
)
}
match texture_bundle {
Some(bundle) =>
match mesh.uv_sets.get(bundle.primary_texcoord_set) {
Some(primary_uvs) =>
if primary_uvs.length() == mesh.positions.length() &&
normals.length() == mesh.positions.length() {
let base_input = resolve_material_texture_input(
mesh,
bundle.base_source,
primary_uvs,
transform_binding=material.base_color_texture,
)
let emissive_input = resolve_material_texture_input(
mesh,
bundle.emissive_source,
primary_uvs,
transform_binding=material.emissive_texture,
)
let metallic_roughness_input = resolve_material_texture_input(
mesh,
bundle.metallic_roughness_source,
primary_uvs,
transform_binding=material.metallic_roughness_texture,
)
let occlusion_input = resolve_material_texture_input(
mesh,
bundle.occlusion_source,
primary_uvs,
transform_binding=material.occlusion_texture,
)
let normal_input = resolve_material_texture_input(
mesh,
bundle.normal_source,
primary_uvs,
transform_binding=material.normal_texture,
)
webgpu_draw_textured_triangles_3d(
base_input.path,
emissive_input.path,
metallic_roughness_input.path,
occlusion_input.path,
if tangents.length() == mesh.positions.length() {
normal_input.path
} else {
""
},
base_input.sampler_code,
emissive_input.sampler_code,
metallic_roughness_input.sampler_code,
occlusion_input.sampler_code,
normal_input.sampler_code,
flatten_textured_vertices3d(
transformed_positions,
triangle_indices,
base_input.uvs,
emissive_input.uvs,
metallic_roughness_input.uvs,
occlusion_input.uvs,
normal_input.uvs,
transformed_normals,
rotate_tangents3d(tangents, rotation, scale),
mesh.colors,
@smath.Vec3::one(),
material,
color,
receive_shadows,
),
double_sided,
cast_shadows,
)
} else {
draw_colored()
}
None => draw_colored()
}
None => draw_colored()
}
}
}
///|
fn cached_textured_primitive_mesh3d(
key : String,
build : () -> @render3d_types.TriangleMesh3D,
) -> @render3d_types.TriangleMesh3D {
textured_primitive_mesh_cache.get_or_init(key, build)
}
///|
fn textured_primitive_cache_key(
prefix : String,
values : Array[String],
) -> String {
let mut key = prefix
for value in values {
key = key + "|" + value
}
key
}
///|
fn cached_cuboid_triangle_mesh3d(
size_x : Double,
size_y : Double,
size_z : Double,
uv_set_count : Int,
) -> @render3d_types.TriangleMesh3D {
cached_textured_primitive_mesh3d(
textured_primitive_cache_key("cuboid", [
size_x.to_string(),
size_y.to_string(),
size_z.to_string(),
uv_set_count.to_string(),
]),
fn() { build_cuboid_triangle_mesh3d(size_x, size_y, size_z, uv_set_count~) },
)
}
///|
fn cached_sphere_triangle_mesh3d(
radius : Double,
slices : Int,
stacks : Int,
uv_set_count : Int,
) -> @render3d_types.TriangleMesh3D {
cached_textured_primitive_mesh3d(
textured_primitive_cache_key("sphere", [
radius.to_string(),
slices.to_string(),
stacks.to_string(),
uv_set_count.to_string(),
]),
fn() { build_sphere_triangle_mesh3d(radius, slices, stacks, uv_set_count~) },
)
}
///|
fn cached_cylinder_triangle_mesh3d(
radius_top : Double,
radius_bottom : Double,
height : Double,
slices : Int,
uv_set_count : Int,
) -> @render3d_types.TriangleMesh3D {
cached_textured_primitive_mesh3d(
textured_primitive_cache_key("cylinder", [
radius_top.to_string(),
radius_bottom.to_string(),
height.to_string(),
slices.to_string(),
uv_set_count.to_string(),
]),
fn() {
build_cylinder_triangle_mesh3d(
radius_top,
radius_bottom,
height,
slices,
uv_set_count~,
)
},
)
}
///|
fn build_cuboid_triangle_mesh3d(
size_x : Double,
size_y : Double,
size_z : Double,
uv_set_count? : Int = 1,
) -> @render3d_types.TriangleMesh3D {
let positions : Array[@smath.Vec3] = []
let uvs : Array[@smath.Vec2] = []
let normals : Array[@smath.Vec3] = []
let half_x = size_x / 2.0
let half_y = size_y / 2.0
let half_z = size_z / 2.0
let p000 = @smath.Vec3(-half_x, -half_y, -half_z)
let p001 = @smath.Vec3(-half_x, -half_y, half_z)
let p010 = @smath.Vec3(-half_x, half_y, -half_z)
let p011 = @smath.Vec3(-half_x, half_y, half_z)
let p100 = @smath.Vec3(half_x, -half_y, -half_z)
let p101 = @smath.Vec3(half_x, -half_y, half_z)
let p110 = @smath.Vec3(half_x, half_y, -half_z)
let p111 = @smath.Vec3(half_x, half_y, half_z)
let uv00 = @smath.Vec2(0.0, 0.0)
let uv10 = @smath.Vec2(1.0, 0.0)
let uv01 = @smath.Vec2(0.0, 1.0)
let uv11 = @smath.Vec2(1.0, 1.0)
let normal_front = @smath.Vec3(0.0, 0.0, 1.0)
push_textured_triangle3d(
positions, uvs, normals, p001, p101, p111, uv01, uv11, uv10, normal_front, normal_front,
normal_front,
)
push_textured_triangle3d(
positions, uvs, normals, p001, p111, p011, uv01, uv10, uv00, normal_front, normal_front,
normal_front,
)
let normal_back = @smath.Vec3(0.0, 0.0, -1.0)
push_textured_triangle3d(
positions, uvs, normals, p100, p000, p010, uv01, uv11, uv10, normal_back, normal_back,
normal_back,
)
push_textured_triangle3d(
positions, uvs, normals, p100, p010, p110, uv01, uv10, uv00, normal_back, normal_back,
normal_back,
)
let normal_right = @smath.Vec3(1.0, 0.0, 0.0)
push_textured_triangle3d(
positions, uvs, normals, p101, p100, p110, uv01, uv11, uv10, normal_right, normal_right,
normal_right,
)
push_textured_triangle3d(
positions, uvs, normals, p101, p110, p111, uv01, uv10, uv00, normal_right, normal_right,
normal_right,
)
let normal_left = @smath.Vec3(-1.0, 0.0, 0.0)
push_textured_triangle3d(
positions, uvs, normals, p000, p001, p011, uv01, uv11, uv10, normal_left, normal_left,
normal_left,
)
push_textured_triangle3d(
positions, uvs, normals, p000, p011, p010, uv01, uv10, uv00, normal_left, normal_left,
normal_left,
)
let normal_top = @smath.Vec3(0.0, 1.0, 0.0)
push_textured_triangle3d(
positions, uvs, normals, p011, p111, p110, uv01, uv11, uv10, normal_top, normal_top,
normal_top,
)
push_textured_triangle3d(
positions, uvs, normals, p011, p110, p010, uv01, uv10, uv00, normal_top, normal_top,
normal_top,
)
let normal_bottom = @smath.Vec3(0.0, -1.0, 0.0)
push_textured_triangle3d(
positions, uvs, normals, p000, p100, p101, uv01, uv11, uv10, normal_bottom, normal_bottom,
normal_bottom,
)
push_textured_triangle3d(
positions, uvs, normals, p000, p101, p001, uv01, uv10, uv00, normal_bottom, normal_bottom,
normal_bottom,
)
build_textured_triangle_mesh3d(positions, uvs, normals, uv_set_count~)
}
///|
fn build_sphere_triangle_mesh3d(
radius : Double,
slices : Int,
stacks : Int,
uv_set_count? : Int = 1,
) -> @render3d_types.TriangleMesh3D {
let positions : Array[@smath.Vec3] = []
let uvs : Array[@smath.Vec2] = []
let normals : Array[@smath.Vec3] = []
let safe_slices = if slices < 3 { 3 } else { slices }
let safe_stacks = if stacks < 2 { 2 } else { stacks }
for stack in 0.. @render3d_types.TriangleMesh3D {
let positions : Array[@smath.Vec3] = []
let uvs : Array[@smath.Vec2] = []
let normals : Array[@smath.Vec3] = []
let safe_slices = if slices < 3 { 3 } else { slices }
let half_height = 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_height, 0.0)
let top_normal = @smath.Vec3(0.0, 1.0, 0.0)
push_textured_triangle3d(
positions,
uvs,
normals,
top_center,
top1,
top0,
Vec2(0.5, 0.5),
Vec2(0.5 + 0.5 * @math.cos(phi1), 0.5 + 0.5 * @math.sin(phi1)),
Vec2(0.5 + 0.5 * @math.cos(phi0), 0.5 + 0.5 * @math.sin(phi0)),
top_normal,
top_normal,
top_normal,
)
}
if radius_bottom.abs() > 0.000001 {
let bottom_center = @smath.Vec3(0.0, -half_height, 0.0)
let bottom_normal = @smath.Vec3(0.0, -1.0, 0.0)
push_textured_triangle3d(
positions,
uvs,
normals,
bottom_center,
bottom0,
bottom1,
Vec2(0.5, 0.5),
Vec2(0.5 + 0.5 * @math.cos(phi0), 0.5 + 0.5 * @math.sin(phi0)),
Vec2(0.5 + 0.5 * @math.cos(phi1), 0.5 + 0.5 * @math.sin(phi1)),
bottom_normal,
bottom_normal,
bottom_normal,
)
}
}
build_textured_triangle_mesh3d(positions, uvs, normals, uv_set_count~)
}
///|
fn build_textured_triangle_mesh3d(
positions : Array[@smath.Vec3],
uvs : Array[@smath.Vec2],
normals : Array[@smath.Vec3],
uv_set_count? : Int = 1,
) -> @render3d_types.TriangleMesh3D {
let safe_uv_set_count = if uv_set_count < 1 { 1 } else { uv_set_count }
let uv_sets : Array[Array[@smath.Vec2]] = []
for _ in 0.. Array[@smath.Vec3] {
let transformed : Array[@smath.Vec3] = []
for position in positions {
transformed.push(
translation +
rotation.rotate_vec3(
Vec3(position.x * scale.x, position.y * scale.y, position.z * scale.z),
),
)
}
transformed
}
///|
fn rotate_normals3d(
normals : Array[@smath.Vec3],
rotation : @smath.Quat,
scale : @smath.Vec3,
) -> Array[@smath.Vec3] {
let transformed : Array[@smath.Vec3] = []
for normal in normals {
let rotated = rotation
.rotate_vec3(inverse_scale_direction3d(normal, scale))
.normalize()
transformed.push(
if rotated.length_squared() <= 0.00000001 {
Vec3(0.0, 1.0, 0.0)
} else {
rotated
},
)
}
transformed
}
///|
fn rotate_tangents3d(
tangents : Array[@render3d_types.Tangent3D],
rotation : @smath.Quat,
scale : @smath.Vec3,
) -> Array[@render3d_types.Tangent3D] {
let transformed : Array[@render3d_types.Tangent3D] = []
for tangent in tangents {
let rotated = rotation
.rotate_vec3(
inverse_scale_direction3d(Vec3(tangent.x, tangent.y, tangent.z), scale),
)
.normalize()
let safe = if rotated.length_squared() <= 0.00000001 {
@smath.Vec3(1.0, 0.0, 0.0)
} else {
rotated
}
transformed.push({ x: safe.x, y: safe.y, z: safe.z, w: tangent.w })
}
transformed
}
///|
fn inverse_scale_direction3d(
direction : @smath.Vec3,
scale : @smath.Vec3,
) -> @smath.Vec3 {
Vec3(
if scale.x.abs() > 0.00000001 {
direction.x / scale.x
} else {
direction.x
},
if scale.y.abs() > 0.00000001 {
direction.y / scale.y
} else {
direction.y
},
if scale.z.abs() > 0.00000001 {
direction.z / scale.z
} else {
direction.z
},
)
}
///|
fn required_primitive_uv_set_count(bundle : MaterialTextureBundle3D) -> Int {
let mut max_set = bundle.primary_texcoord_set
let track = fn(source : MaterialTextureSource3D?) {
match source {
Some(source) =>
if source.texcoord_set > max_set {
max_set = source.texcoord_set
}
None => ()
}
}
track(bundle.base_source)
track(bundle.emissive_source)
track(bundle.metallic_roughness_source)
track(bundle.occlusion_source)
track(bundle.normal_source)
if max_set < 0 {
1
} else {
max_set + 1
}
}
///|
fn sphere_vertex_normal(position : @smath.Vec3) -> @smath.Vec3 {
if position.length_squared() <= 0.0000001 {
Vec3(0.0, 1.0, 0.0)
} else {
position.normalize()
}
}
///|
fn push_textured_triangle3d(
positions : 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 {
positions.push(a)
positions.push(b)
positions.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)
}
///|
fn configure_scene_lighting(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
aspect : Double,
) -> Unit {
let has_lighting = frame.ambient_light is Some(_) ||
frame.directional_lights.length() > 0 ||
frame.point_lights.length() > 0 ||
frame.spot_lights.length() > 0
let ambient = if has_lighting {
frame.ambient_light.map_or(@smath.Vec3::zero(), fn(light) {
let (r, g, b) = color_rgb01(light.color)
@smath.Vec3(r, g, b).scalar_mul(light.intensity)
})
} else {
@smath.Vec3::one()
}
let mode = current_render3d_lighting_mode.val
let light_uniform = pack_light_uniform_data(frame, camera, ambient, mode~)
webgpu_set_lighting_3d(
light_uniform,
pack_directional_shadow_metadata3d(frame, mode~),
pack_point_shadow_metadata3d(frame, mode~),
pack_spot_shadow_metadata3d(frame, mode~),
frame.directional_shadow_map_size.to_double(),
frame.point_shadow_map_size.to_double(),
)
webgpu_set_shadow_view_projection_3d(
pack_directional_shadow_cascade_data(frame, aspect, mode~),
pack_spot_shadow_view_projection_data(frame, mode~),
pack_point_shadow_view_projection_data(frame, mode~),
)
}
///|
fn pack_directional_shadow_metadata3d(
frame : @render3d_types.RenderFrame3D,
mode? : Render3DLightingMode = Full,
) -> Array[Double] {
let values : Array[Double] = []
let count = min_int(
frame.directional_lights.length(),
max_directional_lights_for_mode(mode),
)
for idx in 0.. Array[Double] {
let values : Array[Double] = []
let count = min_int(
frame.point_lights.length(),
max_point_lights_for_mode(mode),
)
for idx in 0.. Array[Double] {
let values : Array[Double] = []
let count = min_int(
frame.spot_lights.length(),
max_spot_lights_for_mode(mode),
)
for idx in 0.. Int {
match mode {
Full => MAX_DIRECTIONAL_LIGHTS
Cheap => 1
Unlit => 0
}
}
///|
fn max_point_lights_for_mode(mode : Render3DLightingMode) -> Int {
match mode {
Full => MAX_POINT_LIGHTS
Cheap | Unlit => 0
}
}
///|
fn max_spot_lights_for_mode(mode : Render3DLightingMode) -> Int {
match mode {
Full => MAX_SPOT_LIGHTS
Cheap | Unlit => 0
}
}
///|
fn pack_light_uniform_data(
frame : @render3d_types.RenderFrame3D,
camera : @render3d_types.FrameCamera3D,
ambient : @smath.Vec3,
mode? : Render3DLightingMode = Full,
) -> Array[Double] {
let values : Array[Double] = []
for _ in 0..<176 {
values.push(0.0)
}
let directional_count = min_int(
frame.directional_lights.length(),
max_directional_lights_for_mode(mode),
)
let point_count = min_int(
frame.point_lights.length(),
max_point_lights_for_mode(mode),
)
let spot_count = min_int(
frame.spot_lights.length(),
max_spot_lights_for_mode(mode),
)
values[0] = clamp01_double(ambient.x)
values[1] = clamp01_double(ambient.y)
values[2] = clamp01_double(ambient.z)
values[3] = directional_count.to_double()
values[4] = point_count.to_double()
values[5] = spot_count.to_double()
values[8] = camera.position.x
values[9] = camera.position.y
values[10] = camera.position.z
let forward = camera_forward3d(camera)
values[12] = forward.x
values[13] = forward.y
values[14] = forward.z
for idx in 0.. @smath.Vec3 {
let forward = camera.target - camera.position
if forward.length_squared() <= 0.0000001 {
Vec3(0.0, 0.0, -1.0)
} else {
forward.normalize()
}
}
///|
fn clamp01_double(value : Double) -> Double {
if value < 0.0 {
0.0
} else if value > 1.0 {
1.0
} else {
value
}
}
///|
fn non_negative_double(value : Double) -> Double {
if value < 0.0 {
0.0
} else {
value
}
}
///|
fn min_int(lhs : Int, rhs : Int) -> Int {
if lhs < rhs {
lhs
} else {
rhs
}
}
///|
fn resolve_mesh_normals(
mesh : @render3d_types.TriangleMesh3D,
) -> Array[@smath.Vec3] {
let triangle_indices = resolve_triangle_indices(mesh).unwrap_or([])
mesh.normals
.filter(fn(candidates) { candidates.length() == mesh.positions.length() })
.unwrap_or(compute_vertex_normals(mesh.positions, triangle_indices))
}
///|
fn resolve_triangle_indices(
mesh : @render3d_types.TriangleMesh3D,
) -> Array[Int]? {
let base_indices = mesh.indices.unwrap_or(
default_mesh_indices(mesh.positions.length()),
)
if base_indices.length() == 0 {
return None
}
for index in base_indices {
if index < 0 || index >= mesh.positions.length() {
return None
}
}
let triangle_indices = triangulate_triangle_indices(
base_indices,
mesh.topology,
)
if triangle_indices.length() < 3 || triangle_indices.length() % 3 != 0 {
return None
}
Some(triangle_indices)
}
///|
fn default_mesh_indices(vertex_count : Int) -> Array[Int] {
let indices : Array[Int] = []
for index in 0.. Array[Int] {
let triangle_indices : Array[Int] = []
let push_index = fn(index : Int) { triangle_indices.push(index) }
match topology {
TriangleList => {
let tri_count = indices.length() / 3
for tri in 0..
if indices.length() >= 3 {
for i in 0..<(indices.length() - 2) {
if i % 2 == 0 {
push_index(indices[i])
push_index(indices[i + 1])
push_index(indices[i + 2])
} else {
push_index(indices[i + 1])
push_index(indices[i])
push_index(indices[i + 2])
}
}
}
TriangleFan =>
if indices.length() >= 3 {
let head = indices[0]
for i in 1..<(indices.length() - 1) {
push_index(head)
push_index(indices[i])
push_index(indices[i + 1])
}
}
}
triangle_indices
}
///|
fn compute_vertex_normals(
positions : Array[@smath.Vec3],
triangle_indices : Array[Int],
) -> Array[@smath.Vec3] {
let normals : Array[@smath.Vec3] = []
for _ in 0..= positions.length() ||
ib < 0 ||
ib >= positions.length() ||
ic < 0 ||
ic >= positions.length() {
continue
}
let a = positions[ia]
let b = positions[ib]
let c = positions[ic]
let mut normal = (b - a).cross(c - a)
if normal.length_squared() <= 0.0000001 {
normal = Vec3(0.0, 1.0, 0.0)
}
normals[ia] = normals[ia] + normal
normals[ib] = normals[ib] + normal
normals[ic] = normals[ic] + normal
}
for index in 0.. Array[@render3d_types.Tangent3D] {
match mesh.tangents {
Some(candidates) if candidates.length() == mesh.positions.length() =>
candidates
_ => {
let tangents : Array[@render3d_types.Tangent3D] = []
for _ in 0.. Array[Double] {
let values : Array[Double] = []
let (emissive_r, emissive_g, emissive_b) = color_rgb01(
material.emissive_color,
)
let alpha_mode = alpha_mode_code(material.alpha_mode)
let alpha_cutoff = material.alpha_cutoff
let unlit = material_unlit_code(material)
for index in triangle_indices {
let position = positions[index]
let normal = normals[index]
let color = resolve_vertex_color(vertex_colors, index, base_color)
let (r, g, b) = color_rgb01(color)
let a = color.a
values.push(position.x * scale.x)
values.push(position.y * scale.y)
values.push(position.z * scale.z)
values.push(normal.x)
values.push(normal.y)
values.push(normal.z)
values.push(r)
values.push(g)
values.push(b)
values.push(a)
values.push(emissive_r)
values.push(emissive_g)
values.push(emissive_b)
values.push(alpha_mode)
values.push(alpha_cutoff)
values.push(unlit)
values.push(if receive_shadows { 1.0 } else { 0.0 })
}
values
}
///|
fn flatten_textured_vertices3d(
positions : Array[@smath.Vec3],
triangle_indices : Array[Int],
base_uvs : Array[@smath.Vec2],
emissive_uvs : Array[@smath.Vec2],
metallic_roughness_uvs : Array[@smath.Vec2],
occlusion_uvs : Array[@smath.Vec2],
normal_uvs : Array[@smath.Vec2],
normals : Array[@smath.Vec3],
tangents : Array[@render3d_types.Tangent3D],
vertex_colors : Array[@render.Color]?,
scale : @smath.Vec3,
material : @render3d_types.StandardMaterial3D,
base_color : @render.Color,
receive_shadows : Bool,
) -> Array[Double] {
let values : Array[Double] = []
let (emissive_r, emissive_g, emissive_b) = color_rgb01(
material.emissive_color,
)
let alpha_mode = alpha_mode_code(material.alpha_mode)
let alpha_cutoff = material.alpha_cutoff
let metallic = material.metallic
let roughness = material.roughness
let occlusion_strength = material.occlusion_strength
let normal_scale = material.normal_scale
let unlit = material_unlit_code(material)
for index in triangle_indices {
let position = positions[index]
let base_uv = base_uvs[index]
let emissive_uv = emissive_uvs[index]
let metallic_roughness_uv = metallic_roughness_uvs[index]
let occlusion_uv = occlusion_uvs[index]
let normal_uv = normal_uvs[index]
let normal = normals[index]
let tangent = tangents[index]
let color = resolve_vertex_color(vertex_colors, index, base_color)
let (r, g, b) = color_rgb01(color)
let a = color.a
values.push(position.x * scale.x)
values.push(position.y * scale.y)
values.push(position.z * scale.z)
values.push(normal.x)
values.push(normal.y)
values.push(normal.z)
values.push(base_uv[X])
values.push(base_uv[Y])
values.push(emissive_uv[X])
values.push(emissive_uv[Y])
values.push(metallic_roughness_uv[X])
values.push(metallic_roughness_uv[Y])
values.push(occlusion_uv[X])
values.push(occlusion_uv[Y])
values.push(normal_uv[X])
values.push(normal_uv[Y])
values.push(r)
values.push(g)
values.push(b)
values.push(a)
values.push(emissive_r)
values.push(emissive_g)
values.push(emissive_b)
values.push(tangent.x)
values.push(tangent.y)
values.push(tangent.z)
values.push(tangent.w)
values.push(alpha_mode)
values.push(alpha_cutoff)
values.push(metallic)
values.push(roughness)
values.push(occlusion_strength)
values.push(normal_scale)
values.push(unlit)
values.push(if receive_shadows { 1.0 } else { 0.0 })
}
values
}
///|
fn resolve_material_texture_input(
mesh : @render3d_types.TriangleMesh3D,
source : MaterialTextureSource3D?,
fallback_uvs : Array[@smath.Vec2],
transform_binding? : @render3d_types.TextureBinding3D? = None,
) -> ResolvedMaterialTextureInput3D {
match source {
Some(source) =>
match mesh.uv_sets.get(source.texcoord_set) {
Some(uvs) if uvs.length() == mesh.positions.length() =>
{
path: source.path,
uvs: resolve_material_texture_uvs(
transform_binding,
source.texcoord_set,
uvs,
),
sampler_code: source.sampler_code,
}
_ => { path: "", uvs: fallback_uvs, sampler_code: 0 }
}
None => { path: "", uvs: fallback_uvs, sampler_code: 0 }
}
}
///|
fn resolve_material_texture_uvs(
binding : @render3d_types.TextureBinding3D?,
texcoord_set : Int,
uvs : Array[@smath.Vec2],
) -> Array[@smath.Vec2] {
match binding {
Some(binding) =>
if binding.texcoord_set == texcoord_set &&
binding.transform != @render3d_types.default_texture_transform3d() {
uvs.map(fn(uv) {
@render3d_types.apply_texture_transform3d(binding.transform, uv)
})
} else {
uvs
}
_ => uvs
}
}
///|
fn color_rgb01(color : @render.Color) -> (Double, Double, Double) {
(
color.r.reinterpret_as_int().to_double() / 255.0,
color.g.reinterpret_as_int().to_double() / 255.0,
color.b.reinterpret_as_int().to_double() / 255.0,
)
}
///|
fn alpha_mode_code(mode : @render3d_types.AlphaMode3D) -> Double {
match mode {
Opaque => 0.0
Mask => 1.0
Blend => 2.0
}
}
///|
fn material_unlit_code(material : @render3d_types.StandardMaterial3D) -> Double {
if material.unlit {
1.0
} else {
0.0
}
}
///|
fn effective_material_color(
material : @render3d_types.StandardMaterial3D,
) -> @render.Color {
match material.alpha_mode {
Opaque => { ..material.base_color, a: 1.0 }
_ => material.base_color
}
}
///|
fn resolve_vertex_color(
vertex_colors : Array[@render.Color]?,
index : Int,
base_color : @render.Color,
) -> @render.Color {
match vertex_colors.bind(fn(values) { values.get(index) }) {
Some(vertex_color) => multiply_colors(base_color, vertex_color)
None => base_color
}
}
///|
fn multiply_colors(lhs : @render.Color, rhs : @render.Color) -> @render.Color {
{
r: ((lhs.r.reinterpret_as_int() * rhs.r.reinterpret_as_int() + 127) / 255).reinterpret_as_uint(),
g: ((lhs.g.reinterpret_as_int() * rhs.g.reinterpret_as_int() + 127) / 255).reinterpret_as_uint(),
b: ((lhs.b.reinterpret_as_int() * rhs.b.reinterpret_as_int() + 127) / 255).reinterpret_as_uint(),
a: lhs.a * rhs.a,
}
}
///|
fn texture_sampler_code(sampler : @render3d_types.TextureSampler3D) -> Int {
let repeat_u = sampler.wrap_u != ClampToEdge
let repeat_v = sampler.wrap_v != ClampToEdge
if repeat_u && repeat_v {
3
} else if repeat_u {
1
} else if repeat_v {
2
} else {
0
}
}
///|
fn resolve_texture_source(
binding : @render3d_types.TextureBinding3D,
) -> MaterialTextureSource3D? {
match synced_image_assets.get(binding.image) {
Some(image_asset) =>
Some({
path: resolve_asset_path(image_asset.path),
texcoord_set: binding.texcoord_set,
sampler_code: texture_sampler_code(binding.sampler),
})
None => None
}
}
///|
fn resolve_material_texture_bundle(
material : @render3d_types.StandardMaterial3D,
) -> MaterialTextureBundle3D? {
let base_source = material.base_color_texture.bind(resolve_texture_source)
let emissive_source = material.emissive_texture.bind(resolve_texture_source)
let metallic_roughness_source = material.metallic_roughness_texture.bind(
resolve_texture_source,
)
let occlusion_source = material.occlusion_texture.bind(resolve_texture_source)
let normal_source = material.normal_texture.bind(resolve_texture_source)
let primary = match base_source {
Some(base) => Some(base)
None =>
match emissive_source {
Some(emissive) => Some(emissive)
None =>
match metallic_roughness_source {
Some(metallic_roughness) => Some(metallic_roughness)
None =>
match occlusion_source {
Some(occlusion) => Some(occlusion)
None => normal_source
}
}
}
}
match primary {
Some(primary_source) =>
Some({
base_source,
emissive_source,
metallic_roughness_source,
occlusion_source,
normal_source,
primary_texcoord_set: primary_source.texcoord_set,
})
None => None
}
}