use crate::Context;
use crate::ShaderOpts;
use crate::drawable::{DrawCommand, DrawCommand3D};
use crate::graphics::model_raw::MaterialBindGroupKey;
use crate::image_raw::InstanceData;
use crate::pt::Pt;
use std::collections::HashMap;
use super::core::{AtlasSlot, Graphics, ResolvedDraw, SkinData};
use crate::graphics::model_raw::{MeshData, ModelRenderer};
use crate::image::ImageEntry;
use crate::image_raw::ImageRenderer;
pub(crate) struct RenderConfig<'a> {
pub screen_size_data: [f32; 4],
pub width: u32,
pub height: u32,
pub sf: f64,
pub atlases: &'a [AtlasSlot],
pub image_pipelines: &'a HashMap<u32, wgpu::RenderPipeline>,
pub default_pipeline: &'a wgpu::RenderPipeline,
}
pub(crate) struct Render3DConfig<'a> {
pub model_pipeline: &'a wgpu::RenderPipeline,
pub instanced_model_pipeline: &'a wgpu::RenderPipeline,
pub shadow_pipeline: &'a wgpu::RenderPipeline,
pub instanced_shadow_pipeline: &'a wgpu::RenderPipeline,
pub model_pipelines: &'a HashMap<u32, wgpu::RenderPipeline>,
pub instanced_model_pipelines: &'a 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>,
);
fn resolve_material_texture<'a>(
images: &[Option<ImageEntry>],
atlases: &'a [AtlasSlot],
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 atlas_index = entry.atlas_index?;
let view = &atlases.get(atlas_index as usize)?.texture.0.view;
let uv_rect = entry.uv_rect.unwrap_or([0.0, 0.0, 1.0, 1.0]);
Some((atlas_index, uv_rect, view))
}
fn expect_image_pipeline<'a>(
image_pipelines: &'a HashMap<u32, wgpu::RenderPipeline>,
default_pipeline: &'a wgpu::RenderPipeline,
shader_id: u32,
) -> &'a wgpu::RenderPipeline {
if shader_id == 0 {
default_pipeline
} else {
image_pipelines.get(&shader_id).unwrap_or_else(|| {
panic!(
"[spot][render] missing image pipeline for shader_id {}",
shader_id
)
})
}
}
fn expect_atlas_bind_group(atlases: &[AtlasSlot], atlas_index: Option<u32>) -> &wgpu::BindGroup {
let atlas_index =
atlas_index.unwrap_or_else(|| panic!("[spot][render] missing atlas index for image batch"));
&atlases
.get(atlas_index as usize)
.unwrap_or_else(|| panic!("[spot][render] missing atlas {}", atlas_index))
.bind_group
}
fn expect_default_material_texture<'a>(
images: &[Option<ImageEntry>],
atlases: &'a [AtlasSlot],
image_id: u32,
label: &str,
) -> MaterialTextureBinding<'a> {
resolve_material_texture(images, atlases, 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>],
atlases: &'a [AtlasSlot],
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, atlases, white_image_id, "white");
let black = expect_default_material_texture(images, atlases, black_image_id, "black");
let normal_default =
expect_default_material_texture(images, atlases, normal_image_id, "normal");
let albedo = resolve_material_texture(images, atlases, part.material.albedo, white_image_id)
.unwrap_or(white);
let pbr = resolve_material_texture(images, atlases, part.material.pbr, black_image_id)
.unwrap_or(black);
let normal = resolve_material_texture(images, atlases, part.material.normal, normal_image_id)
.unwrap_or(normal_default);
let ao = resolve_material_texture(images, atlases, part.material.occlusion, white_image_id)
.unwrap_or(white);
let emissive =
resolve_material_texture(images, atlases, part.material.emissive, black_image_id)
.unwrap_or(black);
(albedo, pbr, normal, ao, emissive)
}
#[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,
}
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),
},
}
}
impl Graphics {
fn process_image_commands(&mut self, ctx: &mut Context, drawables: &[DrawCommand]) {
for command in drawables {
match command {
DrawCommand::ClearImage(target_id, color) => {
if let Err(e) = self.clear_image(ctx, *target_id, *color) {
eprintln!(
"[spot][graphics] clear_image failed for {}: {:?}",
target_id, e
);
}
}
DrawCommand::CopyImage(dst_id, src_id) => {
if let Err(e) = self.copy_image(ctx, *dst_id, *src_id) {
eprintln!(
"[spot][graphics] copy_image failed from {} to {}: {:?}",
src_id, dst_id, e
);
}
}
DrawCommand::Image(_) | DrawCommand::Text(_, _) => {}
}
}
}
pub(crate) fn resolve_drawables(
&mut self,
ctx: &mut Context,
drawables: &[DrawCommand],
logical_w: u32,
logical_h: u32,
) {
self.resolved_draws.clear();
let viewport_rect = [0.0, 0.0, logical_w as f32, logical_h as f32];
for drawable in drawables {
match drawable {
DrawCommand::Image(cmd) => {
if let Some(Some(entry)) = ctx.registry.images.get(cmd.id as usize) {
if !entry.visible || !entry.is_ready() {
continue;
}
self.resolved_draws.push(ResolvedDraw {
img_entry: entry.clone(),
opts: cmd.opts,
shader_id: cmd.shader_id,
shader_opts: cmd.shader_opts,
layer: cmd.opts.layer(),
});
}
}
DrawCommand::Text(text, opts) => {
if let Err(e) = self.layout_and_queue_text(ctx, text, opts, viewport_rect) {
eprintln!("[spot] Text layout error: {:?}", e);
}
}
DrawCommand::ClearImage(_, _) | DrawCommand::CopyImage(_, _) => {}
}
}
}
pub(crate) fn render_batches_internal<'a>(
image_renderer: &mut ImageRenderer,
queue: &wgpu::Queue,
batch: &mut Vec<InstanceData>,
resolved_draws: &mut [ResolvedDraw],
rpass: &mut wgpu::RenderPass<'a>,
config: RenderConfig<'a>,
) {
resolved_draws.sort_by(|a, b| {
a.layer
.cmp(&b.layer)
.then(a.img_entry.atlas_index.cmp(&b.img_entry.atlas_index))
.then(a.shader_id.cmp(&b.shader_id))
});
let mut current_opacity = 1.0f32;
let engine_globals = crate::image_raw::EngineGlobals {
screen: config.screen_size_data,
opacity: current_opacity,
shader_opacity: 1.0,
_padding: [0.0; 2],
};
let mut current_engine_globals_offset = image_renderer
.upload_engine_globals(queue, &engine_globals)
.unwrap_or(0);
let default_user_globals = ShaderOpts::default();
let mut current_user_globals_offset = image_renderer
.upload_user_globals_bytes(queue, default_user_globals.as_bytes())
.unwrap_or(0);
batch.clear();
let mut current_atlas_index: Option<u32> = None;
let mut current_shader_id: u32 = 0;
let mut current_user_globals = ShaderOpts::default();
let mut current_clip: Option<[Pt; 4]> = None;
rpass.set_scissor_rect(0, 0, config.width.max(1), config.height.max(1));
let mut last_set_scissor: Option<(u32, u32, u32, u32)> = None;
for resolved in resolved_draws.iter() {
let img_entry = &resolved.img_entry;
let opts = resolved.opts;
let shader_id = resolved.shader_id;
let shader_opts = resolved.shader_opts;
let draw_opacity = opts.opacity();
let uv_rect = match img_entry.uv_rect {
Some(uv) => uv,
None => continue,
};
let effective_user_globals = shader_opts;
let state_changed = current_atlas_index != img_entry.atlas_index
|| current_shader_id != shader_id
|| current_user_globals != effective_user_globals
|| current_clip != opts.get_clip()
|| current_opacity != draw_opacity;
if state_changed && !batch.is_empty() {
if let Ok(range) = image_renderer.upload_instances(queue, batch.as_slice()) {
let pipeline = expect_image_pipeline(
config.image_pipelines,
config.default_pipeline,
current_shader_id,
);
let atlas_bg = expect_atlas_bind_group(config.atlases, current_atlas_index);
image_renderer.draw_batch(
rpass,
pipeline,
atlas_bg,
range,
current_user_globals_offset,
current_engine_globals_offset,
);
}
batch.clear();
}
if current_opacity != draw_opacity
|| current_user_globals.opacity != resolved.shader_opts.opacity
{
current_opacity = draw_opacity;
let eg = crate::image_raw::EngineGlobals {
screen: config.screen_size_data,
opacity: current_opacity,
shader_opacity: resolved.shader_opts.opacity,
_padding: [0.0; 2],
};
current_engine_globals_offset = image_renderer
.upload_engine_globals(queue, &eg)
.unwrap_or(0);
}
if current_user_globals != effective_user_globals
|| (current_atlas_index.is_none() && batch.is_empty())
{
current_user_globals = effective_user_globals;
current_user_globals_offset = image_renderer
.upload_user_globals_bytes(queue, current_user_globals.as_bytes())
.unwrap_or(current_user_globals_offset);
}
if current_clip != opts.get_clip() {
current_clip = opts.get_clip();
let (sx, sy, sw, sh) = if let Some(clip) = current_clip {
let x0 = (clip[0].as_f32() * config.sf as f32).clamp(0.0, config.width as f32);
let y0 = (clip[1].as_f32() * config.sf as f32).clamp(0.0, config.height as f32);
let x1 = ((clip[0].as_f32() + clip[2].as_f32()) * config.sf as f32)
.clamp(0.0, config.width as f32);
let y1 = ((clip[1].as_f32() + clip[3].as_f32()) * config.sf as f32)
.clamp(0.0, config.height as f32);
let fw = (x1 - x0).max(0.0) as u32;
let fh = (y1 - y0).max(0.0) as u32;
if fw > 0 && fh > 0 {
(x0 as u32, y0 as u32, fw, fh)
} else {
(0, 0, 1, 1)
}
} else {
(0, 0, config.width, config.height)
};
if last_set_scissor != Some((sx, sy, sw, sh)) {
rpass.set_scissor_rect(sx, sy, sw, sh);
last_set_scissor = Some((sx, sy, sw, sh));
}
}
current_atlas_index = img_entry.atlas_index;
current_shader_id = shader_id;
batch.push(InstanceData {
pos: [opts.position()[0].as_f32(), opts.position()[1].as_f32()],
rotation: opts.rotation(),
size: [
img_entry.bounds.width.as_f32() * opts.scale()[0],
img_entry.bounds.height.as_f32() * opts.scale()[1],
],
uv_rect,
});
}
if !batch.is_empty()
&& let Ok(range) = image_renderer.upload_instances(queue, batch.as_slice())
{
let pipeline = expect_image_pipeline(
config.image_pipelines,
config.default_pipeline,
current_shader_id,
);
let atlas_bg = expect_atlas_bind_group(config.atlases, current_atlas_index);
image_renderer.draw_batch(
rpass,
pipeline,
atlas_bg,
range,
current_user_globals_offset,
current_engine_globals_offset,
);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_3d_internal<'a>(
model_renderer: &mut ModelRenderer,
queue: &wgpu::Queue,
device: &wgpu::Device,
scene_globals: &mut crate::graphics::model_raw::SceneGlobals,
models: &[Option<MeshData>],
skins: &[Option<SkinData>],
images: &[Option<ImageEntry>],
atlases: &[AtlasSlot],
config: Render3DConfig,
rpass: &mut wgpu::RenderPass<'a>,
ctx: &Context,
is_shadow_pass: bool,
) {
let mut camera = ctx.runtime.camera;
camera.aspect = config.width as f32 / config.height as f32;
let proj = camera.projection_matrix();
let view_mat = camera.view_matrix();
scene_globals.camera_pos = [camera.eye[0], camera.eye[1], camera.eye[2], 1.0];
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;
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_bone_offset: Option<u32> = None;
let mut environment_bound = false;
let mut opaque_commands = Vec::with_capacity(ctx.runtime.draw_list_3d.len());
let mut transparent_commands = Vec::new();
for command in &ctx.runtime.draw_list_3d {
if draw_command_3d_is_transparent(command) {
transparent_commands.push(command);
} else {
opaque_commands.push(command);
}
}
opaque_commands.sort_by_key(|command| draw_command_3d_sort_key(command));
for command in opaque_commands
.into_iter()
.chain(transparent_commands.into_iter())
{
match command {
crate::drawable::DrawCommand3D::Model(
model,
opts,
shader_id,
shader_opts,
skin_id_cmd,
) => {
let model_mat = crate::graphics::model_raw::create_translation(opts.position);
let rot_mat = crate::graphics::model_raw::create_rotation(opts.rotation);
let scale_mat = crate::graphics::model_raw::create_scale(opts.scale);
let model_mat_all = crate::graphics::model_raw::multiply(
model_mat,
crate::graphics::model_raw::multiply(rot_mat, scale_mat),
);
let mvp = if is_shadow_pass {
crate::graphics::model_raw::multiply(lvp, model_mat_all)
} else {
crate::graphics::model_raw::multiply(
proj,
crate::graphics::model_raw::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()
};
for part in &model.parts {
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,
atlases,
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 = if is_shadow_pass {
config.shadow_pipeline
} else if *shader_id == 0 {
config.model_pipeline
} else {
config
.model_pipelines
.get(shader_id)
.unwrap_or(config.model_pipeline)
};
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));
}
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 {
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 {
if let Ok(opts_offset) = model_renderer
.upload_shader_opts_bytes(queue, shader_opts.as_bytes())
{
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, 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 {
atlas_indices: [
albedo.0, pbr.0, normal.0, ao.0, emissive.0,
],
};
let tex_bg = model_renderer.texture_bind_group_for_atlases(
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(
model,
opts,
shader_id,
shader_opts,
skin_id_cmd,
transforms,
) => {
let model_mat = crate::graphics::model_raw::create_translation(opts.position);
let rot_mat = crate::graphics::model_raw::create_rotation(opts.rotation);
let scale_mat = crate::graphics::model_raw::create_scale(opts.scale);
let model_mat_all = crate::graphics::model_raw::multiply(
model_mat,
crate::graphics::model_raw::multiply(rot_mat, scale_mat),
);
let mvp = if is_shadow_pass {
crate::graphics::model_raw::multiply(lvp, model_mat_all)
} else {
crate::graphics::model_raw::multiply(
proj,
crate::graphics::model_raw::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()
};
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 {
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,
atlases,
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 = if is_shadow_pass {
config.instanced_shadow_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 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));
}
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 {
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 {
if let Ok(opts_offset) = model_renderer
.upload_shader_opts_bytes(queue, shader_opts.as_bytes())
{
rpass.set_bind_group(
0,
&model_renderer.globals_bind_group,
&[offset, 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 {
atlas_indices: [
albedo.0, pbr.0, normal.0, ao.0, emissive.0,
],
};
let tex_bg = model_renderer.texture_bind_group_for_atlases(
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,
);
}
}
}
}
}
}
}
pub fn draw_context(
&mut self,
surface: &wgpu::Surface<'_>,
ctx: &mut Context,
) -> Result<(), wgpu::SurfaceError> {
self.sync_assets(ctx).map_err(|e| {
eprintln!("[spot][graphics] sync_assets failed: {:?}", e);
wgpu::SurfaceError::Lost
})?;
let _ = self.process_registrations(ctx);
let draws = ctx.draw_list().to_vec();
self.process_image_commands(ctx, &draws);
let sf = ctx.scale_factor();
self.draw_drawables_with_context(surface, &draws, sf, ctx)
}
fn draw_drawables_with_context(
&mut self,
surface: &wgpu::Surface<'_>,
drawables: &[DrawCommand],
scale_factor: f64,
ctx: &mut Context,
) -> Result<(), wgpu::SurfaceError> {
let (_lw, _lh) = ctx.window_logical_size();
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
self.draw_drawables_internal(surface, drawables, sf, Some(ctx))
}
pub(crate) fn draw_drawables_internal(
&mut self,
surface: &wgpu::Surface<'_>,
drawables: &[DrawCommand],
scale_factor: f64,
mut ctx: Option<&mut Context>,
) -> Result<(), wgpu::SurfaceError> {
let frame = match surface.get_current_texture() {
Ok(f) => f,
Err(e) => {
eprintln!("[spot][graphics] get_current_texture failed: {:?}", e);
return Err(e);
}
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("command_encoder"),
});
self.model_renderer.begin_frame();
self.image_renderer.begin_frame();
let width = self.config.width;
let height = self.config.height;
if let Some(ref mut ctx) = ctx
&& !ctx.runtime.draw_list_3d.is_empty()
{
let mut shadow_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("shadow_encoder"),
});
{
let mut rpass = shadow_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shadow_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.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,
});
Self::render_3d_internal(
&mut self.model_renderer,
&self.queue,
&self.device,
&mut self.scene_globals,
&self.gpu_models,
&self.gpu_skins,
&ctx.registry.images,
&self.atlases,
Render3DConfig {
model_pipeline: &self.model_pipeline,
instanced_model_pipeline: &self.instanced_model_pipeline,
shadow_pipeline: &self.shadow_pipeline,
instanced_shadow_pipeline: &self.instanced_shadow_pipeline,
model_pipelines: &self.model_pipelines,
instanced_model_pipelines: &self.instanced_model_pipelines,
white_image_id: self.white_image_id,
black_image_id: self.black_image_id,
normal_image_id: self.normal_image_id,
environment_bind_group: &self.environment_bind_group,
width,
height,
},
&mut rpass,
ctx,
true,
);
}
self.queue.submit(std::iter::once(shadow_encoder.finish()));
}
{
if let Some(ref mut ctx) = ctx {
self.resolve_drawables(ctx, drawables, width, height);
}
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("main_render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: if self.transparent { 0.0 } else { 1.0 },
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_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,
});
if let Some(ref mut ctx) = ctx {
Self::render_3d_internal(
&mut self.model_renderer,
&self.queue,
&self.device,
&mut self.scene_globals,
&self.gpu_models,
&self.gpu_skins,
&ctx.registry.images,
&self.atlases,
Render3DConfig {
model_pipeline: &self.model_pipeline,
instanced_model_pipeline: &self.instanced_model_pipeline,
shadow_pipeline: &self.shadow_pipeline,
instanced_shadow_pipeline: &self.instanced_shadow_pipeline,
model_pipelines: &self.model_pipelines,
instanced_model_pipelines: &self.instanced_model_pipelines,
white_image_id: self.white_image_id,
black_image_id: self.black_image_id,
normal_image_id: self.normal_image_id,
environment_bind_group: &self.environment_bind_group,
width,
height,
},
&mut rpass,
ctx,
false,
);
}
let lw = width as f32 / scale_factor as f32;
let lh = height as f32 / scale_factor as f32;
let screen_size_data = [2.0 / lw, 2.0 / lh, 1.0 / lw, 1.0 / lh];
Self::render_batches_internal(
&mut self.image_renderer,
&self.queue,
&mut self.batch,
&mut self.resolved_draws,
&mut rpass,
RenderConfig {
screen_size_data,
width,
height,
sf: scale_factor,
atlases: &self.atlases,
image_pipelines: &self.image_pipelines,
default_pipeline: &self.default_pipeline,
},
);
}
self.queue.submit(std::iter::once(encoder.finish()));
frame.present();
Ok(())
}
}