use crate::Context;
use crate::drawable::DrawCommand3D;
use crate::graphics::model_raw::MaterialBindGroupKey;
use crate::graphics::model_raw::{MeshData, ModelRenderer};
use crate::image::ImageEntry;
use super::core::Graphics;
use super::image_ops::resolve_image_uv;
use crate::model::SkinData;
pub(super) struct Render3DConfig<'a> {
pub model_pipeline: &'a wgpu::RenderPipeline,
pub transparent_model_pipeline: &'a wgpu::RenderPipeline,
pub instanced_model_pipeline: &'a wgpu::RenderPipeline,
pub transparent_instanced_model_pipeline: &'a wgpu::RenderPipeline,
pub shadow_pipeline: &'a wgpu::RenderPipeline,
pub instanced_shadow_pipeline: &'a wgpu::RenderPipeline,
pub model_pipelines: &'a std::collections::HashMap<u32, wgpu::RenderPipeline>,
pub transparent_model_pipelines: &'a std::collections::HashMap<u32, wgpu::RenderPipeline>,
pub instanced_model_pipelines: &'a std::collections::HashMap<u32, wgpu::RenderPipeline>,
pub transparent_instanced_model_pipelines:
&'a std::collections::HashMap<u32, wgpu::RenderPipeline>,
pub white_image_id: u32,
pub black_image_id: u32,
pub normal_image_id: u32,
pub environment_bind_group: &'a wgpu::BindGroup,
pub width: u32,
pub height: u32,
}
type MaterialTextureBinding<'a> = (u32, [f32; 4], &'a wgpu::TextureView);
type MaterialTextureSet<'a> = (
MaterialTextureBinding<'a>,
MaterialTextureBinding<'a>,
MaterialTextureBinding<'a>,
MaterialTextureBinding<'a>,
MaterialTextureBinding<'a>,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct DrawCommand3DSortKey {
instanced: bool,
shader_id: u32,
skin_id: u32,
mesh_id: u32,
albedo_id: u32,
pbr_id: u32,
normal_id: u32,
ao_id: u32,
emissive_id: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct ShadowDrawCommand3DSortKey {
instanced: bool,
skin_id: u32,
mesh_id: u32,
}
fn normalize3(v: [f32; 3]) -> [f32; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(0.0001);
[v[0] / len, v[1] / len, v[2] / len]
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn resolve_material_texture<'a>(
images: &[Option<ImageEntry>],
textures: &'a [Option<crate::graphics::texture::TextureEntry>],
img_id: Option<u32>,
fallback_id: u32,
) -> Option<(u32, [f32; 4], &'a wgpu::TextureView)> {
let id = img_id
.filter(|&id| images.get(id as usize).and_then(|v| v.as_ref()).is_some())
.unwrap_or(fallback_id);
let entry = images.get(id as usize).and_then(|v| v.as_ref())?;
let texture_entry = textures
.get(entry.texture_id as usize)
.and_then(|v| v.as_ref())?;
let uv_rect = resolve_image_uv(entry, texture_entry);
let view = &texture_entry.runtime.gpu_texture.as_ref()?.0.view;
Some((entry.texture_id, uv_rect, view))
}
fn expect_default_material_texture<'a>(
images: &[Option<ImageEntry>],
textures: &'a [Option<crate::graphics::texture::TextureEntry>],
image_id: u32,
label: &str,
) -> MaterialTextureBinding<'a> {
resolve_material_texture(images, textures, Some(image_id), image_id).unwrap_or_else(|| {
panic!(
"[spot][render] default {} texture {} is unavailable",
label, image_id
)
})
}
fn resolve_material_textures<'a>(
images: &[Option<ImageEntry>],
textures: &'a [Option<crate::graphics::texture::TextureEntry>],
part: &crate::model::ModelPart,
white_image_id: u32,
black_image_id: u32,
normal_image_id: u32,
) -> MaterialTextureSet<'a> {
let white = expect_default_material_texture(images, textures, white_image_id, "white");
let black = expect_default_material_texture(images, textures, black_image_id, "black");
let normal_default =
expect_default_material_texture(images, textures, normal_image_id, "normal");
let albedo = resolve_material_texture(images, textures, part.material.albedo, white_image_id)
.unwrap_or(white);
let pbr = resolve_material_texture(images, textures, part.material.pbr, black_image_id)
.unwrap_or(black);
let normal = resolve_material_texture(images, textures, part.material.normal, normal_image_id)
.unwrap_or(normal_default);
let ao = resolve_material_texture(images, textures, part.material.occlusion, white_image_id)
.unwrap_or(white);
let emissive =
resolve_material_texture(images, textures, part.material.emissive, black_image_id)
.unwrap_or(black);
(albedo, pbr, normal, ao, emissive)
}
fn draw_command_3d_is_transparent(command: &DrawCommand3D) -> bool {
match command {
DrawCommand3D::Model(_, _, opts, ..) | DrawCommand3D::ModelInstanced(_, _, opts, ..) => {
opts.opacity < 1.0
}
}
}
fn draw_command_3d_sort_key(command: &DrawCommand3D) -> DrawCommand3DSortKey {
match command {
DrawCommand3D::Model(_, model, _, shader_id, _, skin_id) => DrawCommand3DSortKey {
instanced: false,
shader_id: *shader_id,
skin_id: skin_id.unwrap_or(0),
mesh_id: model.first_id(),
albedo_id: model
.parts
.first()
.and_then(|part| part.material.albedo)
.unwrap_or(0),
pbr_id: model
.parts
.first()
.and_then(|part| part.material.pbr)
.unwrap_or(0),
normal_id: model
.parts
.first()
.and_then(|part| part.material.normal)
.unwrap_or(0),
ao_id: model
.parts
.first()
.and_then(|part| part.material.occlusion)
.unwrap_or(0),
emissive_id: model
.parts
.first()
.and_then(|part| part.material.emissive)
.unwrap_or(0),
},
DrawCommand3D::ModelInstanced(_, model, _, shader_id, _, skin_id, _) => {
DrawCommand3DSortKey {
instanced: true,
shader_id: *shader_id,
skin_id: skin_id.unwrap_or(0),
mesh_id: model.first_id(),
albedo_id: model
.parts
.first()
.and_then(|part| part.material.albedo)
.unwrap_or(0),
pbr_id: model
.parts
.first()
.and_then(|part| part.material.pbr)
.unwrap_or(0),
normal_id: model
.parts
.first()
.and_then(|part| part.material.normal)
.unwrap_or(0),
ao_id: model
.parts
.first()
.and_then(|part| part.material.occlusion)
.unwrap_or(0),
emissive_id: model
.parts
.first()
.and_then(|part| part.material.emissive)
.unwrap_or(0),
}
}
}
}
fn draw_command_3d_position(command: &DrawCommand3D) -> [f32; 3] {
match command {
DrawCommand3D::Model(_, _, opts, ..) | DrawCommand3D::ModelInstanced(_, _, opts, ..) => {
opts.position
}
}
}
fn shadow_draw_command_3d_sort_key(command: &DrawCommand3D) -> ShadowDrawCommand3DSortKey {
match command {
DrawCommand3D::Model(_, model, _, _, _, skin_id)
| DrawCommand3D::ModelInstanced(_, model, _, _, _, skin_id, _) => {
ShadowDrawCommand3DSortKey {
instanced: matches!(command, DrawCommand3D::ModelInstanced(..)),
skin_id: skin_id.unwrap_or(0),
mesh_id: model.first_id(),
}
}
}
}
fn distance_squared(a: [f32; 3], b: [f32; 3]) -> f32 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
dx * dx + dy * dy + dz * dz
}
fn draw_option_3d_model_matrix(opts: &crate::DrawOption3D) -> [[f32; 4]; 4] {
let model_mat = crate::math::mat4::from_translation(opts.position);
let rot_mat = crate::math::mat4::from_rotation(opts.rotation);
let scale_mat = crate::math::mat4::from_scale(opts.scale);
crate::math::mat4::multiply(model_mat, crate::math::mat4::multiply(rot_mat, scale_mat))
}
impl Graphics {
pub(super) fn clear_3d_command_order(&mut self) {
if let Some(model_3d) = self.model_3d_mut() {
model_3d.opaque_draw_indices_3d.clear();
model_3d.transparent_draw_indices_3d.clear();
model_3d.shadow_draw_indices_3d.clear();
}
}
#[allow(clippy::too_many_arguments)]
fn render_shadow_3d_internal<'pass>(
model_renderer: &mut ModelRenderer,
queue: &wgpu::Queue,
models: &[Option<MeshData>],
skins: &[Option<SkinData>],
pipeline: &wgpu::RenderPipeline,
rpass: &mut wgpu::RenderPass<'pass>,
ctx: &Context,
shadow_draw_indices: &[usize],
target_texture_id: u32,
lvp: [[f32; 4]; 4],
) {
let mut current_mesh_id: Option<u32> = None;
let mut current_bone_offset: Option<u32> = None;
rpass.set_pipeline(pipeline);
let mut index = 0;
while index < shadow_draw_indices.len() {
let Some(command) = ctx
.runtime
.model_3d
.draw_list
.get(shadow_draw_indices[index])
else {
index += 1;
continue;
};
match command {
DrawCommand3D::Model(command_target_texture_id, model, opts, _, _, skin_id_cmd) => {
if *command_target_texture_id != target_texture_id {
index += 1;
continue;
}
let mut transforms = Vec::with_capacity(8);
transforms.push(draw_option_3d_model_matrix(opts));
let mut next_index = index + 1;
while next_index < shadow_draw_indices.len() {
match ctx
.runtime
.model_3d
.draw_list
.get(shadow_draw_indices[next_index])
{
Some(DrawCommand3D::Model(
next_target_texture_id,
next_model,
next_opts,
_,
_,
next_skin_id,
)) if *next_target_texture_id == target_texture_id
&& next_skin_id == skin_id_cmd
&& next_model == model =>
{
transforms.push(draw_option_3d_model_matrix(next_opts));
next_index += 1;
}
Some(_) | None => break,
}
}
if let Err(e) = model_renderer.upload_instances(queue, &transforms) {
eprintln!("[spot][render] Failed to upload shadow instances: {}", e);
index = next_index;
continue;
}
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd
&& let Some(Some(skin)) = skins.get(*skin_id as usize)
&& let Ok(off) = model_renderer.bone_offset_for_skin(
queue,
*skin_id,
&skin.bone_matrices,
)
{
bone_offset = off;
}
let globals = crate::graphics::model_raw::ModelGlobals {
mvp: lvp,
model: crate::math::mat4::identity(),
extra: [1.0, 0.0, 0.0, 0.0],
..Default::default()
};
if let Ok(offset) = model_renderer.upload_globals(queue, &globals) {
rpass.set_bind_group(0, &model_renderer.globals_bind_group, &[offset, 0]);
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
1,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
for part in model.parts.iter() {
if let Some(Some(mesh)) = models.get(part.id as usize) {
if current_mesh_id != Some(part.id) {
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_vertex_buffer(
1,
model_renderer.instance_buffer.slice(..),
);
rpass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
current_mesh_id = Some(part.id);
}
rpass.draw_indexed(
0..mesh.index_count,
0,
0..transforms.len() as u32,
);
}
}
}
index = next_index;
}
DrawCommand3D::ModelInstanced(
command_target_texture_id,
model,
opts,
_,
_,
skin_id_cmd,
transforms,
) => {
if *command_target_texture_id != target_texture_id {
index += 1;
continue;
}
if let Err(e) = model_renderer.upload_instances(queue, transforms.as_ref()) {
eprintln!("[spot][render] Failed to upload instances: {}", e);
index += 1;
continue;
}
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd
&& let Some(Some(skin)) = skins.get(*skin_id as usize)
&& let Ok(off) = model_renderer.bone_offset_for_skin(
queue,
*skin_id,
&skin.bone_matrices,
)
{
bone_offset = off;
}
let globals = crate::graphics::model_raw::ModelGlobals {
mvp: crate::math::mat4::multiply(lvp, draw_option_3d_model_matrix(opts)),
model: crate::math::mat4::identity(),
extra: [1.0, 0.0, 0.0, 0.0],
..Default::default()
};
if let Ok(offset) = model_renderer.upload_globals(queue, &globals) {
rpass.set_bind_group(0, &model_renderer.globals_bind_group, &[offset, 0]);
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
1,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
for part in model.parts.iter() {
if let Some(Some(mesh)) = models.get(part.id as usize) {
if current_mesh_id != Some(part.id) {
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_vertex_buffer(
1,
model_renderer.instance_buffer.slice(..),
);
rpass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
current_mesh_id = Some(part.id);
}
rpass.draw_indexed(
0..mesh.index_count,
0,
0..transforms.len() as u32,
);
}
}
}
index += 1;
}
}
}
}
pub(super) fn prepare_3d_command_order(&mut self, ctx: &Context, target_texture_id: u32) {
let model_3d = self.ensure_model_3d();
model_3d.opaque_draw_indices_3d.clear();
model_3d.transparent_draw_indices_3d.clear();
model_3d.shadow_draw_indices_3d.clear();
model_3d
.opaque_draw_indices_3d
.reserve(ctx.runtime.model_3d.draw_list.len());
model_3d
.transparent_draw_indices_3d
.reserve(ctx.runtime.model_3d.draw_list.len());
model_3d
.shadow_draw_indices_3d
.reserve(ctx.runtime.model_3d.draw_list.len());
for (index, command) in ctx.runtime.model_3d.draw_list.iter().enumerate() {
let command_target = match command {
DrawCommand3D::Model(target, ..) | DrawCommand3D::ModelInstanced(target, ..) => {
*target
}
};
if command_target != target_texture_id {
continue;
}
model_3d.shadow_draw_indices_3d.push(index);
if draw_command_3d_is_transparent(command) {
model_3d.transparent_draw_indices_3d.push(index);
} else {
model_3d.opaque_draw_indices_3d.push(index);
}
}
model_3d
.opaque_draw_indices_3d
.sort_by_key(|&index| draw_command_3d_sort_key(&ctx.runtime.model_3d.draw_list[index]));
let camera_pos = ctx.runtime.model_3d.camera.eye;
model_3d.transparent_draw_indices_3d.sort_by(|&a, &b| {
let command_a = &ctx.runtime.model_3d.draw_list[a];
let command_b = &ctx.runtime.model_3d.draw_list[b];
let distance_a = distance_squared(camera_pos, draw_command_3d_position(command_a));
let distance_b = distance_squared(camera_pos, draw_command_3d_position(command_b));
distance_b
.partial_cmp(&distance_a)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
draw_command_3d_sort_key(command_a).cmp(&draw_command_3d_sort_key(command_b))
})
});
model_3d.shadow_draw_indices_3d.sort_by_key(|&index| {
shadow_draw_command_3d_sort_key(&ctx.runtime.model_3d.draw_list[index])
});
}
pub(super) fn render_shadow_pass(
&mut self,
encoder: &mut wgpu::CommandEncoder,
ctx: &mut Context,
width: u32,
height: u32,
target_texture_id: u32,
) {
self.ensure_model_3d();
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shadow_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.model_3d().expect("ensured").shadow_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
let queue = &self.queue;
let device = &self.device;
let model_3d = self.model_3d.as_mut().expect("ensured");
Self::render_3d_internal(
&mut model_3d.model_renderer,
queue,
device,
&mut model_3d.scene_globals,
&model_3d.gpu_models,
&model_3d.gpu_skins,
&ctx.registry.images,
&ctx.registry.textures,
Render3DConfig {
model_pipeline: &model_3d.model_pipeline,
transparent_model_pipeline: &model_3d.transparent_model_pipeline,
instanced_model_pipeline: &model_3d.instanced_model_pipeline,
transparent_instanced_model_pipeline: &model_3d
.transparent_instanced_model_pipeline,
shadow_pipeline: &model_3d.shadow_pipeline,
instanced_shadow_pipeline: &model_3d.instanced_shadow_pipeline,
model_pipelines: &model_3d.model_pipelines,
transparent_model_pipelines: &model_3d.transparent_model_pipelines,
instanced_model_pipelines: &model_3d.instanced_model_pipelines,
transparent_instanced_model_pipelines: &model_3d
.transparent_instanced_model_pipelines,
white_image_id: model_3d.white_image_id,
black_image_id: model_3d.black_image_id,
normal_image_id: model_3d.normal_image_id,
environment_bind_group: &model_3d.environment_bind_group,
width,
height,
},
&mut rpass,
ctx,
&model_3d.shadow_draw_indices_3d,
&[],
true,
target_texture_id,
);
}
}
pub(super) fn render_main_3d_pass<'pass>(
&mut self,
ctx: &mut Context,
width: u32,
height: u32,
rpass: &mut wgpu::RenderPass<'pass>,
target_texture_id: u32,
) {
let queue = &self.queue;
let device = &self.device;
let model_3d = self.model_3d.as_mut().expect("ensured");
Self::render_3d_internal(
&mut model_3d.model_renderer,
queue,
device,
&mut model_3d.scene_globals,
&model_3d.gpu_models,
&model_3d.gpu_skins,
&ctx.registry.images,
&ctx.registry.textures,
Render3DConfig {
model_pipeline: &model_3d.model_pipeline,
transparent_model_pipeline: &model_3d.transparent_model_pipeline,
instanced_model_pipeline: &model_3d.instanced_model_pipeline,
transparent_instanced_model_pipeline: &model_3d
.transparent_instanced_model_pipeline,
shadow_pipeline: &model_3d.shadow_pipeline,
instanced_shadow_pipeline: &model_3d.instanced_shadow_pipeline,
model_pipelines: &model_3d.model_pipelines,
transparent_model_pipelines: &model_3d.transparent_model_pipelines,
instanced_model_pipelines: &model_3d.instanced_model_pipelines,
transparent_instanced_model_pipelines: &model_3d
.transparent_instanced_model_pipelines,
white_image_id: model_3d.white_image_id,
black_image_id: model_3d.black_image_id,
normal_image_id: model_3d.normal_image_id,
environment_bind_group: &model_3d.environment_bind_group,
width,
height,
},
rpass,
ctx,
&model_3d.opaque_draw_indices_3d,
&model_3d.transparent_draw_indices_3d,
false,
target_texture_id,
);
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_3d_internal<'pass, 'cfg>(
model_renderer: &mut ModelRenderer,
queue: &wgpu::Queue,
device: &wgpu::Device,
scene_globals: &mut crate::model::SceneGlobals,
models: &[Option<MeshData>],
skins: &[Option<SkinData>],
images: &[Option<ImageEntry>],
textures: &[Option<crate::graphics::texture::TextureEntry>],
config: Render3DConfig<'cfg>,
rpass: &mut wgpu::RenderPass<'pass>,
ctx: &Context,
opaque_draw_indices: &[usize],
transparent_draw_indices: &[usize],
is_shadow_pass: bool,
target_texture_id: u32,
) {
let mut camera = ctx.runtime.model_3d.camera;
camera.aspect = config.width as f32 / config.height as f32;
let proj = camera.projection_matrix();
let view_mat = camera.view_matrix();
let forward = normalize3([
camera.target[0] - camera.eye[0],
camera.target[1] - camera.eye[1],
camera.target[2] - camera.eye[2],
]);
let right = normalize3(cross3(camera.up, forward));
let up = cross3(forward, right);
scene_globals.camera_pos = [camera.eye[0], camera.eye[1], camera.eye[2], 1.0];
scene_globals.camera_right = [right[0], right[1], right[2], 0.0];
scene_globals.camera_up = [up[0], up[1], up[2], 0.0];
scene_globals.camera_forward = [forward[0], forward[1], forward[2], 0.0];
scene_globals.projection_params = [proj[0][0], proj[1][1], camera.znear, camera.zfar];
scene_globals.light_view_proj = [
[0.1, 0.0, 0.0, 0.0],
[0.0, 0.1, 0.0, 0.0],
[0.0, 0.0, 0.05, 0.0],
[0.0, 0.0, 0.5, 1.0],
];
if !is_shadow_pass {
model_renderer.upload_scene_globals(queue, scene_globals);
}
let lvp = scene_globals.light_view_proj;
if is_shadow_pass {
Self::render_shadow_3d_internal(
model_renderer,
queue,
models,
skins,
config.instanced_shadow_pipeline,
rpass,
ctx,
opaque_draw_indices,
target_texture_id,
lvp,
);
return;
}
let mut current_pipeline: Option<*const wgpu::RenderPipeline> = None;
let mut current_mesh_binding: Option<(u32, bool)> = None;
let mut current_material_key: Option<MaterialBindGroupKey> = None;
let mut current_shader_opts_bytes: Option<[u8; ModelRenderer::USER_SHADER_OPTS_SIZE]> =
None;
let mut current_shader_opts_offset: u32 = 0;
let mut current_bone_offset: Option<u32> = None;
let mut environment_bound = false;
for command in opaque_draw_indices
.iter()
.chain(transparent_draw_indices.iter())
.filter_map(|&index| ctx.runtime.model_3d.draw_list.get(index))
{
match command {
DrawCommand3D::Model(
command_target_texture_id,
model,
opts,
shader_id,
shader_opts,
skin_id_cmd,
) => {
if *command_target_texture_id != target_texture_id {
continue;
}
let model_mat = crate::math::mat4::from_translation(opts.position);
let rot_mat = crate::math::mat4::from_rotation(opts.rotation);
let scale_mat = crate::math::mat4::from_scale(opts.scale);
let model_mat_all = crate::math::mat4::multiply(
model_mat,
crate::math::mat4::multiply(rot_mat, scale_mat),
);
let mvp = if is_shadow_pass {
crate::math::mat4::multiply(lvp, model_mat_all)
} else {
crate::math::mat4::multiply(
proj,
crate::math::mat4::multiply(view_mat, model_mat_all),
)
};
let base_globals = crate::graphics::model_raw::ModelGlobals {
mvp,
model: model_mat_all,
extra: [opts.opacity, 0.0, 0.0, 0.0],
..Default::default()
};
let is_transparent = opts.opacity < 1.0;
let pipeline = if is_shadow_pass {
config.shadow_pipeline
} else if is_transparent && *shader_id == 0 {
config.transparent_model_pipeline
} else if is_transparent {
config
.transparent_model_pipelines
.get(shader_id)
.unwrap_or(config.transparent_model_pipeline)
} else if *shader_id == 0 {
config.model_pipeline
} else {
config
.model_pipelines
.get(shader_id)
.unwrap_or(config.model_pipeline)
};
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd
&& let Some(Some(skin)) = skins.get(*skin_id as usize)
&& let Ok(off) = model_renderer.bone_offset_for_skin(
queue,
*skin_id,
&skin.bone_matrices,
)
{
bone_offset = off;
}
if !is_shadow_pass {
let shader_opts_bytes: [u8; ModelRenderer::USER_SHADER_OPTS_SIZE] =
shader_opts
.as_bytes()
.try_into()
.expect("model shader opts size should match renderer buffer size");
if current_shader_opts_bytes != Some(shader_opts_bytes)
&& let Ok(offset) =
model_renderer.upload_shader_opts_bytes(queue, &shader_opts_bytes)
{
current_shader_opts_bytes = Some(shader_opts_bytes);
current_shader_opts_offset = offset;
}
}
for part in model.parts.iter() {
if let Some(Some(mesh)) = models.get(part.id as usize) {
let mut globals = base_globals;
let mut material_texture_data = None;
if !is_shadow_pass {
let (albedo, pbr, normal, ao, emissive) = resolve_material_textures(
images,
textures,
part,
config.white_image_id,
config.black_image_id,
config.normal_image_id,
);
globals.albedo_uv = albedo.1;
globals.pbr_uv = pbr.1;
globals.normal_uv = normal.1;
globals.ao_uv = ao.1;
globals.emissive_uv = emissive.1;
material_texture_data = Some((albedo, pbr, normal, ao, emissive));
}
if let Ok(offset) = model_renderer.upload_globals(queue, &globals) {
let pipeline_ptr = pipeline as *const wgpu::RenderPipeline;
if current_pipeline != Some(pipeline_ptr) {
rpass.set_pipeline(pipeline);
current_pipeline = Some(pipeline_ptr);
}
if current_mesh_binding != Some((part.id, false)) {
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
current_mesh_binding = Some((part.id, false));
}
if is_shadow_pass {
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, 0],
);
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
1,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
} else {
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, current_shader_opts_offset],
);
let (albedo, pbr, normal, ao, emissive) = material_texture_data
.unwrap_or_else(|| {
panic!(
"[spot][render] material textures missing after resolution for mesh {}",
part.id
)
});
let material_key = MaterialBindGroupKey {
texture_source_ids: [
albedo.0, pbr.0, normal.0, ao.0, emissive.0,
],
};
let tex_bg = model_renderer.texture_bind_group_for_materials(
device,
material_key,
[albedo.2, pbr.2, normal.2, ao.2, emissive.2],
);
if current_material_key != Some(material_key) {
rpass.set_bind_group(1, tex_bg, &[]);
current_material_key = Some(material_key);
}
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
2,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
if !environment_bound {
rpass.set_bind_group(3, config.environment_bind_group, &[]);
environment_bound = true;
}
}
rpass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
}
}
DrawCommand3D::ModelInstanced(
command_target_texture_id,
model,
opts,
shader_id,
shader_opts,
skin_id_cmd,
transforms,
) => {
if *command_target_texture_id != target_texture_id {
continue;
}
let model_mat = crate::math::mat4::from_translation(opts.position);
let rot_mat = crate::math::mat4::from_rotation(opts.rotation);
let scale_mat = crate::math::mat4::from_scale(opts.scale);
let model_mat_all = crate::math::mat4::multiply(
model_mat,
crate::math::mat4::multiply(rot_mat, scale_mat),
);
let mvp = if is_shadow_pass {
crate::math::mat4::multiply(lvp, model_mat_all)
} else {
crate::math::mat4::multiply(
proj,
crate::math::mat4::multiply(view_mat, model_mat_all),
)
};
let base_globals = crate::graphics::model_raw::ModelGlobals {
mvp,
model: model_mat_all,
extra: [opts.opacity, 0.0, 0.0, 0.0],
..Default::default()
};
let is_transparent = opts.opacity < 1.0;
let pipeline = if is_shadow_pass {
config.instanced_shadow_pipeline
} else if is_transparent && *shader_id == 0 {
config.transparent_instanced_model_pipeline
} else if is_transparent {
config
.transparent_instanced_model_pipelines
.get(shader_id)
.unwrap_or(config.transparent_instanced_model_pipeline)
} else if *shader_id == 0 {
config.instanced_model_pipeline
} else {
config
.instanced_model_pipelines
.get(shader_id)
.unwrap_or(config.instanced_model_pipeline)
};
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd
&& let Some(Some(skin)) = skins.get(*skin_id as usize)
&& let Ok(off) = model_renderer.bone_offset_for_skin(
queue,
*skin_id,
&skin.bone_matrices,
)
{
bone_offset = off;
}
if !is_shadow_pass {
let shader_opts_bytes: [u8; ModelRenderer::USER_SHADER_OPTS_SIZE] =
shader_opts
.as_bytes()
.try_into()
.expect("model shader opts size should match renderer buffer size");
if current_shader_opts_bytes != Some(shader_opts_bytes)
&& let Ok(offset) =
model_renderer.upload_shader_opts_bytes(queue, &shader_opts_bytes)
{
current_shader_opts_bytes = Some(shader_opts_bytes);
current_shader_opts_offset = offset;
}
}
if let Err(e) = model_renderer.upload_instances(queue, transforms.as_ref()) {
eprintln!("[spot][render] Failed to upload instances: {}", e);
continue;
}
for part in model.parts.iter() {
if let Some(Some(mesh)) = models.get(part.id as usize) {
let mut globals = base_globals;
let mut material_texture_data = None;
if !is_shadow_pass {
let (albedo, pbr, normal, ao, emissive) = resolve_material_textures(
images,
textures,
part,
config.white_image_id,
config.black_image_id,
config.normal_image_id,
);
globals.albedo_uv = albedo.1;
globals.pbr_uv = pbr.1;
globals.normal_uv = normal.1;
globals.ao_uv = ao.1;
globals.emissive_uv = emissive.1;
material_texture_data = Some((albedo, pbr, normal, ao, emissive));
}
if let Ok(offset) = model_renderer.upload_globals(queue, &globals) {
let pipeline_ptr = pipeline as *const wgpu::RenderPipeline;
if current_pipeline != Some(pipeline_ptr) {
rpass.set_pipeline(pipeline);
current_pipeline = Some(pipeline_ptr);
}
if current_mesh_binding != Some((part.id, true)) {
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_vertex_buffer(
1,
model_renderer.instance_buffer.slice(..),
);
rpass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
current_mesh_binding = Some((part.id, true));
}
if is_shadow_pass {
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, 0],
);
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
1,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
} else {
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, current_shader_opts_offset],
);
let (albedo, pbr, normal, ao, emissive) = material_texture_data
.unwrap_or_else(|| {
panic!(
"[spot][render] material textures missing after resolution for instanced mesh {}",
part.id
)
});
let material_key = MaterialBindGroupKey {
texture_source_ids: [
albedo.0, pbr.0, normal.0, ao.0, emissive.0,
],
};
let tex_bg = model_renderer.texture_bind_group_for_materials(
device,
material_key,
[albedo.2, pbr.2, normal.2, ao.2, emissive.2],
);
if current_material_key != Some(material_key) {
rpass.set_bind_group(1, tex_bg, &[]);
current_material_key = Some(material_key);
}
if current_bone_offset != Some(bone_offset) {
rpass.set_bind_group(
2,
&model_renderer.bone_matrices_bind_group,
&[bone_offset],
);
current_bone_offset = Some(bone_offset);
}
if !environment_bound {
rpass.set_bind_group(3, config.environment_bind_group, &[]);
environment_bound = true;
}
}
rpass.draw_indexed(
0..mesh.index_count,
0,
0..transforms.len() as u32,
);
}
}
}
}
}
}
}
}