Étape 14: add shadow mapping (directional light, Phase 4.2)
Implement shadow mapping for directional lights: - Scene::set_shadow_caster(Option<usize>) selects the shadow-casting light by packed frame-array index (None disables; point lights rejected at render). - Lights::get(index) resolves a packed index across the directional/point/spot lists. - Renderer allocates a shadow depth map, comparison sampler, group-3 bind groups, shadow uniform buffer and shadow pipeline; render_scene does a depth-only shadow pass before the main pass; compute_shadow_light_view_proj builds an orthographic light-space frustum from the scene radius. - standard_shader: shadow_light_index/light_view_proj/shadow_params uniforms, @group(3) depth map + comparison sampler, 3x3 PCF compute_shadow(). - shadow_shader: path/vertex shader with attribute layout matching the shared vertex buffer (only position consumed). - shadow_test example: directional shadow caster casts a PCF-softened shadow onto a ground slab; documented in examples README.
This commit is contained in:
+260
-15
@@ -21,11 +21,19 @@
|
||||
use crate::core::Context;
|
||||
use crate::core::Frame;
|
||||
use crate::math::Transform;
|
||||
use crate::pipeline::{DEPTH_FORMAT, create_uniform_bind_group_layouts};
|
||||
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE};
|
||||
use crate::resources::{Camera, FrameUniforms, Lights, Material, Mesh, ObjectUniform};
|
||||
use crate::pipeline::{
|
||||
DEPTH_FORMAT, build_shadow_pipeline, create_shadow_map_bind_group_layout,
|
||||
create_shadow_uniform_layout, create_uniform_bind_group_layouts,
|
||||
};
|
||||
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE, SHADOW_UNIFORM_SIZE};
|
||||
use crate::resources::{
|
||||
Camera, FrameUniforms, Lights, Material, Mesh, ObjectUniform, ShadowUniform, MAX_LIGHTS,
|
||||
};
|
||||
use crate::scene::Scene;
|
||||
use glam::Vec4;
|
||||
use crate::utils::conf::{
|
||||
SHADOW_DEPTH_BIAS, SHADOW_MAP_SIZE, SHADOW_SCENE_CENTER, SHADOW_SCENE_RADIUS,
|
||||
};
|
||||
use glam::{Mat4, Vec3, Vec4};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -67,6 +75,22 @@ pub struct Renderer {
|
||||
/// `FrameUniforms` is set to 1 so the `standard` shader returns vertex colors as-is — flat 2D
|
||||
/// rendering is thus a special case of the 3D lit path (DRAFT Étape 5). Defaults to `false` (lit).
|
||||
unlit: bool,
|
||||
// Étape 14 (DRAFT 3.2) — shadow mapping resources, owned by the Renderer like the depth texture.
|
||||
/// Backing GPU shadow-map texture (D2), kept alive for the whole application lifetime. Sized
|
||||
/// `SHADOW_MAP_SIZE²`, `DEPTH_FORMAT`, used as the shadow pass depth attachment **and** bound
|
||||
/// for sampling in the main pass (`RENDER_ATTACHMENT | TEXTURE_BINDING`).
|
||||
_shadow_texture: wgpu::Texture,
|
||||
/// Depth view of the shadow map, bound into `shadow_bind_group` (group 3) for the PCF test.
|
||||
shadow_view: wgpu::TextureView,
|
||||
/// Group-3 bind group (comparison sampler + shadow depth texture) bound on every main draw call.
|
||||
shadow_bind_group: wgpu::BindGroup,
|
||||
/// Per-frame uniform buffer holding the shadow-casting light's `view_proj` (D3). Rewritten
|
||||
/// each frame before the shadow pass so the depth-only pipeline sees the current light pose.
|
||||
shadow_uniform_buffer: wgpu::Buffer,
|
||||
/// Group-0 bind group of the shadow pipeline (the light `view_proj`, D4).
|
||||
shadow_uniform_bind_group: wgpu::BindGroup,
|
||||
/// Depth-only pipeline rendering the scene from the shadow light's point of view (D4).
|
||||
shadow_pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
@@ -125,6 +149,55 @@ impl Renderer {
|
||||
}],
|
||||
});
|
||||
|
||||
// Étape 14 (DRAFT 3.2) : shadow mapping resources — shadow map texture/view, comparison
|
||||
// sampler, group-3 bind group, shadow-light uniform buffer + group-0 bind group, and the
|
||||
// depth-only shadow pipeline. All allocated once here at the default resolution (D2/D8).
|
||||
let (shadow_texture, shadow_view) = create_shadow_map(&device, SHADOW_MAP_SIZE);
|
||||
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("shadow comparison sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
// Comparison sampler : `textureSampleCompare` returns 1 when the sampled depth passes
|
||||
// this test against the reference, 0 otherwise (D5). GreaterEqual = lit when nothing
|
||||
// closer than the fragment has been written into the shadow map.
|
||||
compare: Some(wgpu::CompareFunction::GreaterEqual),
|
||||
..Default::default()
|
||||
});
|
||||
let shadow_map_layout = create_shadow_map_bind_group_layout(&device);
|
||||
let shadow_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("shadow map bind group"),
|
||||
layout: &shadow_map_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Sampler(&shadow_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&shadow_view),
|
||||
},
|
||||
],
|
||||
});
|
||||
let shadow_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("shadow uniform buffer"),
|
||||
size: SHADOW_UNIFORM_SIZE,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let shadow_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("shadow uniform bind group"),
|
||||
layout: &create_shadow_uniform_layout(&device),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: shadow_uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let shadow_pipeline = build_shadow_pipeline(&device, &object_layout);
|
||||
|
||||
let renderer = Self {
|
||||
queue,
|
||||
device,
|
||||
@@ -137,6 +210,12 @@ impl Renderer {
|
||||
shared_object_bind_group,
|
||||
object_cache: RefCell::new(HashMap::new()),
|
||||
unlit: false,
|
||||
_shadow_texture: shadow_texture,
|
||||
shadow_view,
|
||||
shadow_bind_group,
|
||||
shadow_uniform_buffer,
|
||||
shadow_uniform_bind_group,
|
||||
shadow_pipeline,
|
||||
};
|
||||
// Seed the shared frame buffer with an identity camera + current unlit flag so the low-level
|
||||
// `render` path (which has no window/camera) sees coherent values before `render_scene` runs.
|
||||
@@ -151,6 +230,9 @@ impl Renderer {
|
||||
fn write_default_frame_uniforms(&self) {
|
||||
let frame = FrameUniforms {
|
||||
options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
|
||||
// Étape 14 (D2) : no active shadow caster in the low-level path — sentinel index
|
||||
// MAX_LIGHTS disables the shadow term in the shader even if options.y were set.
|
||||
shadow_light_index: MAX_LIGHTS as u32,
|
||||
..FrameUniforms::default()
|
||||
};
|
||||
self.queue
|
||||
@@ -192,15 +274,28 @@ impl Renderer {
|
||||
/// The light array is packed via `Lights::into_frame_array` (directionals first, then point,
|
||||
/// then spot lights). Inputs: camera (the scene's active camera), lights (the scene's global
|
||||
/// light list), ambient (the scene's ambient hemisphere color, rgb), aspect (viewport width /
|
||||
/// height).
|
||||
/// height), shadow_caster (the packed-array index of the shadow-casting light, from
|
||||
/// `Scene::shadow_caster`, or `None` when shadows are disabled / the caster is a point light).
|
||||
fn write_frame_uniforms(
|
||||
&self,
|
||||
camera: &Camera,
|
||||
lights: &Lights,
|
||||
ambient: [f32; 3],
|
||||
aspect: f32,
|
||||
shadow_caster: Option<usize>,
|
||||
) {
|
||||
let (light_array, num_directional, num_point, num_spot) = lights.into_frame_array();
|
||||
// Étape 14 (DRAFT 3.2) : derive the shadow light's view_proj and shadow flags (D3).
|
||||
let (shadow_light_index, light_view_proj, shadow_params, shadow_on) =
|
||||
match self.shadow_light_view_proj(lights, shadow_caster) {
|
||||
Some((index, vp)) => (
|
||||
index as u32,
|
||||
vp,
|
||||
Vec4::new(SHADOW_MAP_SIZE as f32, SHADOW_DEPTH_BIAS, 0.0, 0.0),
|
||||
1,
|
||||
),
|
||||
None => (MAX_LIGHTS as u32, Mat4::IDENTITY, Vec4::ZERO, 0),
|
||||
};
|
||||
let frame = FrameUniforms {
|
||||
view: camera.view_matrix(),
|
||||
proj: camera.projection_matrix(aspect),
|
||||
@@ -210,13 +305,60 @@ impl Renderer {
|
||||
num_directional,
|
||||
num_point,
|
||||
num_spot,
|
||||
_pad: [0],
|
||||
options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
|
||||
shadow_light_index,
|
||||
light_view_proj,
|
||||
shadow_params,
|
||||
options: [if self.unlit { 1 } else { 0 }, shadow_on, 0, 0],
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
|
||||
}
|
||||
|
||||
/// Computes the light-space orthographic view-projection of the shadow-casting light, plus its
|
||||
/// packed-array index. The volume covered is an orthographic box of half-size
|
||||
/// `SHADOW_SCENE_RADIUS` centered on the scene origin (SHADOW_SCENE_CENTER), oriented so its
|
||||
/// `-z` axis aligns with the light's travel direction (light → scene). Placing the eye behind
|
||||
/// the scene along the light path keeps the frustum locked to the light orientation even when
|
||||
/// the directional light's `position` is arbitrary. The projection uses `near = 0.0` /
|
||||
/// `far = SHADOW_SCENE_RADIUS` so the depth written by the shadow pass matches the `depth` the
|
||||
/// main-pass shader compares (D3). Returns `None` when no valid caster is selected (shadows
|
||||
/// disabled, index out of bounds, or the caster is a point light — D6).
|
||||
fn shadow_light_view_proj(
|
||||
&self,
|
||||
lights: &Lights,
|
||||
caster: Option<usize>,
|
||||
) -> Option<(usize, Mat4)> {
|
||||
let index = caster?;
|
||||
if index >= lights.len() {
|
||||
return None;
|
||||
}
|
||||
let light = lights.get(index)?;
|
||||
// Directional and spot lights carry a direction; point lights would need a 6-face cubemap
|
||||
// shadow, which is out of scope (D6), so we reject them.
|
||||
let dir = match light.light_type() {
|
||||
crate::resources::LightType::Directional
|
||||
| crate::resources::LightType::Spot { .. } => Vec3::new(
|
||||
light.dir_angle.x,
|
||||
light.dir_angle.y,
|
||||
light.dir_angle.z,
|
||||
),
|
||||
crate::resources::LightType::Point => return None,
|
||||
};
|
||||
let r = SHADOW_SCENE_RADIUS;
|
||||
let target = Vec3::from(SHADOW_SCENE_CENTER);
|
||||
// Eye one scene-radius behind the target along the light path, so distance(target)=r and
|
||||
// every point in the box has depth within [near=0, far=r].
|
||||
let eye = target - dir * r;
|
||||
// Avoid a degenerate basis when the light points straight down/up (parallel up vector).
|
||||
let up = if dir.y.abs() > 0.99 { Vec3::Z } else { Vec3::Y };
|
||||
let view = glam::camera::rh::view::look_at_mat4(eye, target, up);
|
||||
// Orthographic box of half-size r, near 0, far r (D1/D3), in the same OpenGL NDC convention
|
||||
// as the camera projection (wgpu maps NDC z ∈ [-1,1] to depth [0,1], see standard_shader).
|
||||
let proj =
|
||||
glam::camera::rh::proj::opengl::orthographic(-r, r, -r, r, 0.0, r);
|
||||
Some((index, proj * view))
|
||||
}
|
||||
|
||||
/// Orchestrates rendering of a single object: binds Material pipeline + Mesh vertex data into a RenderPass,
|
||||
/// then submits commands to the GPU queue for execution. Called per-frame by the orchestrator (main.rs).
|
||||
/// Inputs: view (TextureView color attachment target), mesh (geometry to render), material (shader+pipeline).
|
||||
@@ -265,6 +407,7 @@ impl Renderer {
|
||||
material,
|
||||
&self.frame_bind_group,
|
||||
&self.shared_object_bind_group,
|
||||
&self.shadow_bind_group,
|
||||
);
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
@@ -281,7 +424,13 @@ impl Renderer {
|
||||
/// Before drawing, the shared frame uniform buffer is rewritten from `scene.camera()` so the GPU
|
||||
/// receives the active camera's view/projection matrices and position for this frame (Étape 4.3).
|
||||
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene, aspect: f32) {
|
||||
self.write_frame_uniforms(scene.camera(), scene.lights(), scene.ambient(), aspect);
|
||||
self.write_frame_uniforms(
|
||||
scene.camera(),
|
||||
scene.lights(),
|
||||
scene.ambient(),
|
||||
aspect,
|
||||
scene.shadow_caster(),
|
||||
);
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
@@ -289,6 +438,11 @@ impl Renderer {
|
||||
label: Some("scene encoder"),
|
||||
});
|
||||
|
||||
// Étape 14 (DRAFT 3.2) : run the depth-only shadow pass first when a light is configured to
|
||||
// cast shadows (D4). It populates `shadow_view` on the shared encoder; the main pass below
|
||||
// then samples it via `shadow_bind_group`. `render_shadow_map` no-ops when shadows are off.
|
||||
self.render_shadow_map(&mut encoder, scene);
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("scene render pass"),
|
||||
@@ -328,12 +482,69 @@ impl Renderer {
|
||||
&material,
|
||||
&self.frame_bind_group,
|
||||
&object_bind_group,
|
||||
&self.shadow_bind_group,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
|
||||
/// Renders every entity of `scene` from the shadow-casting light's point of view into the
|
||||
/// shadow depth map (Étape 14, D4), using the dedicated depth-only `shadow_pipeline`. Called at
|
||||
/// the start of `render_scene`. No-ops (produces no GPU work) when `scene.shadow_caster()` is
|
||||
/// `None`. The shadow light's `view_proj` is written to `shadow_uniform_buffer`, and the shadow
|
||||
/// pass writes depth into `shadow_view` (clear 1.0, store). The per-entity model bind groups are
|
||||
/// reused from `object_bind_group_for`, so transforms match the main pass exactly.
|
||||
/// Inputs: encoder (the shared command encoder for the frame), scene (entities to cast).
|
||||
fn render_shadow_map(&self, encoder: &mut wgpu::CommandEncoder, scene: &Scene) {
|
||||
let _caster = match scene.shadow_caster() {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
// Recompute the light's view_proj and write it into the shadow uniform buffer so the
|
||||
// depth-only vertex shader transforms vertices into light-clip space (D4).
|
||||
let (light_index, vp) = match self.shadow_light_view_proj(scene.lights(), Some(_caster)) {
|
||||
Some(pair) => pair,
|
||||
None => return,
|
||||
};
|
||||
let shadow_uniform = ShadowUniform { view_proj: vp };
|
||||
self.queue
|
||||
.write_buffer(&self.shadow_uniform_buffer, 0, bytemuck::bytes_of(&shadow_uniform));
|
||||
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("shadow map render pass"),
|
||||
color_attachments: &[],
|
||||
// Depth-only : the shadow map is the sole attachment. Clear 1.0 so fragments beyond
|
||||
// `far` read as "fully distant" and never occlude lit surfaces (D4).
|
||||
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,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&self.shadow_pipeline);
|
||||
// Group 0 : the shadow light view_proj (D4) — the shadow pipeline's only uniform group.
|
||||
pass.set_bind_group(0, &self.shadow_uniform_bind_group, &[]);
|
||||
for (label, mesh, transform) in scene.iter_entities() {
|
||||
let object_bind_group = self.object_bind_group_for(label, transform);
|
||||
// Group 1 : per-entity model. The shadow pipeline has no texture/sampler groups.
|
||||
pass.set_bind_group(1, &object_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
if let Some(index_buffer) = &mesh.index_buffer {
|
||||
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||
pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
|
||||
} else {
|
||||
pass.draw(0..mesh.num_vertices, 0..1);
|
||||
}
|
||||
}
|
||||
drop(pass);
|
||||
let _ = light_index; // (index retained for future per-light shadow options)
|
||||
}
|
||||
|
||||
/// Presents the rendered frame by submitting the acquired surface texture to the GPU queue.
|
||||
/// The frame must have been obtained via Context::begin_frame() or Frame::try_new(); calling present()
|
||||
/// twice on the same texture is undefined behavior. Called by the orchestrator after render().
|
||||
@@ -419,21 +630,52 @@ fn create_depth_texture(
|
||||
(depth_texture, depth_view)
|
||||
}
|
||||
|
||||
/// Binds a Material pipeline, the three shared bind groups, and Mesh buffers into an active render
|
||||
/// Allocates the shadow-map texture + view backing the depth-only shadow pass's
|
||||
/// `depth_stencil_attachment` (Étape 14, D2/D8). Square (`size` x `size`), `DEPTH_FORMAT`, single
|
||||
/// mip, no MSAA. Unlike the screen depth texture this one is flagged **both** `RENDER_ATTACHMENT`
|
||||
/// (shadow pass writes depth) **and** `TEXTURE_BINDING` (main pass samples it via the group-3
|
||||
/// comparison sampler). Allocated once at the default resolution; resizing is deferred (D8).
|
||||
/// Inputs: device (GPU resource creator), size (shadow map edge length in pixels).
|
||||
/// Returns the (texture, view) pair; the caller keeps both alive.
|
||||
fn create_shadow_map(
|
||||
device: &wgpu::Device,
|
||||
size: u32,
|
||||
) -> (wgpu::Texture, wgpu::TextureView) {
|
||||
let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("shadow map"),
|
||||
size: wgpu::Extent3d {
|
||||
width: size,
|
||||
height: size,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let shadow_view = shadow_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
(shadow_texture, shadow_view)
|
||||
}
|
||||
|
||||
/// Binds a Material pipeline, the four shared bind groups, and Mesh buffers into an active render
|
||||
/// pass and issues the draw call. Shared by `Renderer::render` and `Renderer::render_scene`.
|
||||
/// The frame (@0), object (@1) and texture (@2) bind groups are **required** by every pipeline layout
|
||||
/// (Étape 3 : un seul layout pour tous — Étape 10 : groupe texture) — they must be bound even if the
|
||||
/// shader does not read them. Draws indexed geometry when an index buffer exists, otherwise falls
|
||||
/// back to a non-indexed draw.
|
||||
/// The frame (@0), object (@1), texture (@2) and shadow-map (@3) bind groups are **required** by
|
||||
/// every pipeline layout (Étape 3 : un seul layout pour tous — Étape 10 : groupe texture — Étape 14 :
|
||||
/// groupe ombre) — they must be bound even if the shader does not read them. Draws indexed geometry
|
||||
/// when an index buffer exists, otherwise falls back to a non-indexed draw.
|
||||
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline + texture bind
|
||||
/// group to bind), frame_bind_group (shared per-frame uniforms), object_bind_group (per-entity/identity
|
||||
/// model).
|
||||
/// group to bind), frame_bind_group (shared per-frame uniforms), object_bind_group (per-entity/
|
||||
/// identity model), shadow_bind_group (reserved group-3 shadow-map bind group, unused by the
|
||||
/// depth-only shadow pipeline but required by the standard pipeline layout).
|
||||
fn draw_entity(
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
mesh: &Mesh,
|
||||
material: &Material,
|
||||
frame_bind_group: &wgpu::BindGroup,
|
||||
object_bind_group: &wgpu::BindGroup,
|
||||
shadow_bind_group: &wgpu::BindGroup,
|
||||
) {
|
||||
if mesh.num_vertices == 0 {
|
||||
// No vertices — nothing to render.
|
||||
@@ -445,6 +687,9 @@ fn draw_entity(
|
||||
// Étape 10 (DRAFT 10.4) : groupe texture — le Material possède son bind group (placeholder
|
||||
// blanc s'il n'a pas de texture, D1/D2). Toujours liable car posé sur toutes les pipelines.
|
||||
pass.set_bind_group(2, &material.texture_bind_group, &[]);
|
||||
// Étape 14 : groupe ombre — toujours lié pour rester conforme au layout unifié, que la pipeline
|
||||
// soit éclairée ou non (le groupe @3 reste requis par toutes les pipelines standards).
|
||||
pass.set_bind_group(3, shadow_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
if let Some(index_buffer) = &mesh.index_buffer {
|
||||
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||
|
||||
Reference in New Issue
Block a user