Fix rendering multiple entities
this is done by using a large dynamic uniform buffer for storing all transforms of entities
This commit is contained in:
parent
3068710ba4
commit
ec960b8f94
|
@ -56,6 +56,12 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aligned-vec"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -1075,6 +1081,7 @@ dependencies = [
|
||||||
name = "lyra-engine"
|
name = "lyra-engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aligned-vec",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|
|
@ -29,3 +29,4 @@ syn = "2.0.26"
|
||||||
quote = "1.0.29"
|
quote = "1.0.29"
|
||||||
edict = "0.5.0"
|
edict = "0.5.0"
|
||||||
atomicell = "0.1.9"
|
atomicell = "0.1.9"
|
||||||
|
aligned-vec = "0.5.0"
|
||||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -79,11 +79,22 @@ async fn main() {
|
||||||
indices: Some(crate::render::vertex::INDICES.to_vec())
|
indices: Some(crate::render::vertex::INDICES.to_vec())
|
||||||
}, Material {
|
}, Material {
|
||||||
shader_id: 0,
|
shader_id: 0,
|
||||||
texture: diffuse_texture
|
texture: diffuse_texture.clone()
|
||||||
}),
|
}),
|
||||||
TransformComponent::from(Transform::from_xyz(0.005, 0.0, 0.0)),
|
TransformComponent::from(Transform::from_xyz(0.005, 0.0, 0.0)),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
world.spawn((MeshComponent::new(
|
||||||
|
render::mesh::Mesh {
|
||||||
|
vertices: crate::render::vertex::VERTICES.to_vec(),
|
||||||
|
indices: Some(crate::render::vertex::INDICES.to_vec())
|
||||||
|
}, Material {
|
||||||
|
shader_id: 0,
|
||||||
|
texture: diffuse_texture
|
||||||
|
}),
|
||||||
|
TransformComponent::from(Transform::from_xyz(0.005, 0.7, 0.0)),
|
||||||
|
));
|
||||||
|
|
||||||
let mut camera = CameraComponent::new();
|
let mut camera = CameraComponent::new();
|
||||||
camera.transform.translation += glam::Vec3::new(0.0, 0.0, 2.0);
|
camera.transform.translation += glam::Vec3::new(0.0, 0.0, 2.0);
|
||||||
//camera.transform.rotate_y(Angle::Degrees(-25.0));
|
//camera.transform.rotate_y(Angle::Degrees(-25.0));
|
||||||
|
@ -123,23 +134,14 @@ async fn main() {
|
||||||
|
|
||||||
for transform in world.query_mut::<(&mut TransformComponent,)>().iter_mut() {
|
for transform in world.query_mut::<(&mut TransformComponent,)>().iter_mut() {
|
||||||
let t = &mut transform.transform;
|
let t = &mut transform.transform;
|
||||||
//debug!("Translation: {}", t.translation);
|
debug!("Translation: {}", t.translation);
|
||||||
|
|
||||||
/* t.translation += glam::Vec3::new(0.0, 0.001, 0.0);
|
/* t.translation += glam::Vec3::new(0.0, 0.001, 0.0);
|
||||||
t.translation.x *= -1.0; */
|
t.translation.x *= -1.0; */
|
||||||
t.translation.x += dir_x;
|
t.translation.x += dir_x;
|
||||||
t.translation.y += dir_y;
|
t.translation.y += dir_y;
|
||||||
}
|
}
|
||||||
/* for (transform,) in world.query_mut::<(TransformComponent, )>().iter() {
|
debug!("end");
|
||||||
let t = &mut transform.transform;
|
|
||||||
|
|
||||||
/* debug!("Translation: {}", t.translation);
|
|
||||||
|
|
||||||
t.translation += glam::Vec3::new(0.0, 0.001, 0.0);
|
|
||||||
t.translation.x *= -1.0 */
|
|
||||||
t.translation.x += dir_x;
|
|
||||||
t.translation.y += dir_y;
|
|
||||||
} */
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::mem;
|
||||||
|
use std::num::NonZeroU64;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use aligned_vec::AVec;
|
||||||
use async_std::sync::Mutex;
|
use async_std::sync::Mutex;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use atomicell::{AtomicCell, RefMut};
|
use atomicell::{AtomicCell, RefMut};
|
||||||
use edict::{EntityId, Entities};
|
use edict::{EntityId, Entities};
|
||||||
|
use glam::Mat4;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use wgpu::{BindGroup, BindGroupLayout};
|
use wgpu::{BindGroup, BindGroupLayout, Limits, BufferBinding};
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
|
@ -39,7 +43,75 @@ struct RenderBufferStorage {
|
||||||
|
|
||||||
render_texture: Option<RenderTexture>,
|
render_texture: Option<RenderTexture>,
|
||||||
texture_bindgroup: Option<BindGroup>,
|
texture_bindgroup: Option<BindGroup>,
|
||||||
texture_layout: Option<BindGroupLayout>
|
texture_layout: Option<BindGroupLayout>,
|
||||||
|
|
||||||
|
/// The index of the transform for this entity.
|
||||||
|
/// The tuple is structured like this: (transform index, index of transform inside the buffer)
|
||||||
|
transform_index: TransformBufferIndices,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
struct TransformBufferIndices {
|
||||||
|
buffer_index: usize,
|
||||||
|
transform_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransformBuffers {
|
||||||
|
/// A vector storing the EntityId and
|
||||||
|
just_updated: HashMap<EntityId, TransformBufferIndices>,
|
||||||
|
not_updated: HashMap<EntityId, TransformBufferIndices>,
|
||||||
|
dead_indices: VecDeque<TransformBufferIndices>,
|
||||||
|
next_indices: TransformBufferIndices,
|
||||||
|
buffer_bindgroups: Vec<(wgpu::Buffer, wgpu::BindGroup)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransformBuffers {
|
||||||
|
/// Update an entity's buffer with the new transform. Will panic if the entity isn't stored
|
||||||
|
fn update_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: EntityId, transform: glam::Mat4) {
|
||||||
|
let indices = self.not_updated.remove(&entity)
|
||||||
|
.expect("Use 'insert_entity' for new entities");
|
||||||
|
self.just_updated.insert(entity, indices);
|
||||||
|
|
||||||
|
let (buffer, _) = self.buffer_bindgroups.get(indices.buffer_index).unwrap();
|
||||||
|
queue.write_buffer(buffer, indices.transform_index as u64 * limits.min_uniform_buffer_offset_alignment as u64, bytemuck::bytes_of(&transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new entity into the buffer, returns where it was stored.
|
||||||
|
fn insert_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: EntityId, transform: glam::Mat4) -> TransformBufferIndices {
|
||||||
|
// get a dead index, or create a new one
|
||||||
|
let indices = if let Some(index) = self.dead_indices.pop_front() {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
// TODO: Create new buffer if this one is full
|
||||||
|
let indices = &mut self.next_indices;
|
||||||
|
let new = indices.clone();
|
||||||
|
indices.transform_index += 1;
|
||||||
|
|
||||||
|
new
|
||||||
|
};
|
||||||
|
|
||||||
|
let (buffer, _) = self.buffer_bindgroups.get(indices.buffer_index).unwrap();
|
||||||
|
queue.write_buffer(buffer, Self::get_offset_for(limits, indices), bytemuck::bytes_of(&transform));
|
||||||
|
|
||||||
|
self.just_updated.insert(entity, indices);
|
||||||
|
indices
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect the dead entities, mark entities and not updated for next updates.
|
||||||
|
fn tick(&mut self) {
|
||||||
|
// take the dead entities, these were ones that were not updated this tick
|
||||||
|
let dead: VecDeque<TransformBufferIndices> = self.not_updated.values()
|
||||||
|
.map(|t| t.clone()).collect();
|
||||||
|
self.dead_indices = dead;
|
||||||
|
|
||||||
|
// swap just_updated into not_updated
|
||||||
|
self.not_updated = self.just_updated.clone();
|
||||||
|
self.just_updated.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_offset_for(limits: &Limits, indices: TransformBufferIndices) -> u64 {
|
||||||
|
indices.transform_index as u64 * limits.min_uniform_buffer_offset_alignment as u64
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BasicRenderer {
|
pub struct BasicRenderer {
|
||||||
|
@ -57,8 +129,11 @@ pub struct BasicRenderer {
|
||||||
|
|
||||||
buffer_storage: HashMap<EntityId, RenderBufferStorage>, // TODO: clean up left over buffers from deleted entities/components
|
buffer_storage: HashMap<EntityId, RenderBufferStorage>, // TODO: clean up left over buffers from deleted entities/components
|
||||||
|
|
||||||
transform_buffer: wgpu::Buffer,
|
transform_buffers: TransformBuffers,
|
||||||
transform_bind_group: wgpu::BindGroup,
|
transform_bind_group_layout: BindGroupLayout,
|
||||||
|
//transform_bind_group: wgpu::BindGroup,
|
||||||
|
|
||||||
|
render_limits: Limits,
|
||||||
|
|
||||||
inuse_camera: RenderCamera,
|
inuse_camera: RenderCamera,
|
||||||
camera_buffer: wgpu::Buffer,
|
camera_buffer: wgpu::Buffer,
|
||||||
|
@ -102,6 +177,7 @@ impl BasicRenderer {
|
||||||
None,
|
None,
|
||||||
).await.unwrap();
|
).await.unwrap();
|
||||||
|
|
||||||
|
let render_limits = device.limits();
|
||||||
let surface_caps = surface.get_capabilities(&adapter);
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
|
||||||
let present_mode = surface_caps.present_modes[0]; /* match surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) {
|
let present_mode = surface_caps.present_modes[0]; /* match surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) {
|
||||||
|
@ -158,14 +234,6 @@ impl BasicRenderer {
|
||||||
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(&shader_src)),
|
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(&shader_src)),
|
||||||
});
|
});
|
||||||
|
|
||||||
let transform_buffer = device.create_buffer_init(
|
|
||||||
&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: Some("Transform Buffer"),
|
|
||||||
contents: bytemuck::cast_slice(&[glam::Mat4::IDENTITY]),
|
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let transform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
let transform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupLayoutEntry {
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
@ -173,7 +241,7 @@ impl BasicRenderer {
|
||||||
visibility: wgpu::ShaderStages::VERTEX,
|
visibility: wgpu::ShaderStages::VERTEX,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: true,
|
||||||
min_binding_size: None,
|
min_binding_size: None,
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
|
@ -182,17 +250,43 @@ impl BasicRenderer {
|
||||||
label: Some("transform_bind_group_layout"),
|
label: Some("transform_bind_group_layout"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let transform_buffer = device.create_buffer(
|
||||||
|
&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Transform Buffer 0"),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
size: render_limits.max_uniform_buffer_binding_size as u64,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let stride = render_limits.min_uniform_buffer_offset_alignment as usize + mem::size_of::<glam::Mat4>();
|
||||||
let transform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let transform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
layout: &transform_bind_group_layout,
|
layout: &transform_bind_group_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: transform_buffer.as_entire_binding(),
|
resource: wgpu::BindingResource::Buffer(
|
||||||
|
wgpu::BufferBinding {
|
||||||
|
buffer: &transform_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: Some(NonZeroU64::new(stride as u64).unwrap())
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
label: Some("transform_bind_group"),
|
label: Some("transform_bind_group"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// create create the transform buffer storage
|
||||||
|
//let transforms = AVec::new(render_limits.min_uniform_buffer_offset_alignment as usize);
|
||||||
|
let transform_buffers = TransformBuffers {
|
||||||
|
buffer_bindgroups: vec![( transform_buffer, transform_bind_group )],
|
||||||
|
just_updated: HashMap::new(),
|
||||||
|
not_updated: HashMap::new(),
|
||||||
|
dead_indices: VecDeque::new(),
|
||||||
|
next_indices: TransformBufferIndices { buffer_index: 0, transform_index: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
let camera_buffer = device.create_buffer_init(
|
let camera_buffer = device.create_buffer_init(
|
||||||
&wgpu::util::BufferInitDescriptor {
|
&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("Camera Buffer"),
|
label: Some("Camera Buffer"),
|
||||||
|
@ -226,7 +320,7 @@ impl BasicRenderer {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
label: Some("camera_bind_group"),
|
label: Some("camera_bind_group"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut pipelines = HashMap::new();
|
let mut pipelines = HashMap::new();
|
||||||
pipelines.insert(0, Arc::new(FullRenderPipeline::new(&device, &config, &shader,
|
pipelines.insert(0, Arc::new(FullRenderPipeline::new(&device, &config, &shader,
|
||||||
|
@ -250,8 +344,9 @@ impl BasicRenderer {
|
||||||
render_jobs: VecDeque::new(),
|
render_jobs: VecDeque::new(),
|
||||||
buffer_storage: HashMap::new(),
|
buffer_storage: HashMap::new(),
|
||||||
|
|
||||||
transform_buffer,
|
render_limits,
|
||||||
transform_bind_group,
|
transform_buffers,
|
||||||
|
transform_bind_group_layout,
|
||||||
|
|
||||||
inuse_camera: RenderCamera::new(size),
|
inuse_camera: RenderCamera::new(size),
|
||||||
camera_buffer,
|
camera_buffer,
|
||||||
|
@ -259,7 +354,7 @@ impl BasicRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_model_buffers(&mut self, model_2d: &MeshComponent) -> RenderBufferStorage {
|
fn create_model_buffers(&mut self, model_2d: &MeshComponent, transform_indices: TransformBufferIndices) -> RenderBufferStorage {
|
||||||
let mesh = &model_2d.mesh;
|
let mesh = &model_2d.mesh;
|
||||||
|
|
||||||
let vertex_buffer = self.device.create_buffer_init(
|
let vertex_buffer = self.device.create_buffer_init(
|
||||||
|
@ -340,24 +435,34 @@ impl BasicRenderer {
|
||||||
render_texture: None,
|
render_texture: None,
|
||||||
texture_layout: None,
|
texture_layout: None,
|
||||||
texture_bindgroup: Some(diffuse_bind_group),
|
texture_bindgroup: Some(diffuse_bind_group),
|
||||||
|
transform_index: transform_indices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer for BasicRenderer {
|
impl Renderer for BasicRenderer {
|
||||||
fn prepare(&mut self, main_world: &mut edict::World) {
|
fn prepare(&mut self, main_world: &mut edict::World) {
|
||||||
|
// render_limits.max_uniform_buffer_binding_size
|
||||||
for (entity, model, transform) in main_world.query::<(Entities, &MeshComponent, &TransformComponent)>().iter() {
|
for (entity, model, transform) in main_world.query::<(Entities, &MeshComponent, &TransformComponent)>().iter() {
|
||||||
// Create the render job and push it to the queue
|
// Create the render job and push it to the queue
|
||||||
let job = RenderJob::new(model.mesh.clone(), model.material.clone(), entity, transform.transform, None);
|
let job = RenderJob::new(model.mesh.clone(), model.material.clone(), entity, transform.transform, None);
|
||||||
self.render_jobs.push_back(job);
|
self.render_jobs.push_back(job);
|
||||||
|
|
||||||
if self.buffer_storage.get(&entity).is_none() {
|
if self.buffer_storage.get(&entity).is_none() {
|
||||||
let buffers = self.create_model_buffers(model);
|
let indices = self.transform_buffers.insert_entity(&self.queue, &self.render_limits,
|
||||||
|
entity, transform.transform.calculate_mat4());
|
||||||
|
|
||||||
|
let buffers = self.create_model_buffers(model, indices);
|
||||||
self.buffer_storage.insert(entity, buffers);
|
self.buffer_storage.insert(entity, buffers);
|
||||||
|
} else {
|
||||||
|
self.transform_buffers.update_entity(&self.queue, &self.render_limits,
|
||||||
|
entity, transform.transform.calculate_mat4());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collect dead entities
|
||||||
|
self.transform_buffers.tick();
|
||||||
|
|
||||||
if let Some(camera) = main_world.query_mut::<(&mut CameraComponent,)>().into_iter().next() {
|
if let Some(camera) = main_world.query_mut::<(&mut CameraComponent,)>().into_iter().next() {
|
||||||
let view_proj = self.inuse_camera.update_view_projection(camera);
|
let view_proj = self.inuse_camera.update_view_projection(camera);
|
||||||
self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[view_proj.clone()]));
|
self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[view_proj.clone()]));
|
||||||
|
@ -393,22 +498,28 @@ impl Renderer for BasicRenderer {
|
||||||
// Pop off jobs from the queue as they're being processed
|
// Pop off jobs from the queue as they're being processed
|
||||||
while let Some(job) = self.render_jobs.pop_front() {
|
while let Some(job) = self.render_jobs.pop_front() {
|
||||||
if let Some(pipeline) = self.render_pipelines.get(&job.material().shader_id) {
|
if let Some(pipeline) = self.render_pipelines.get(&job.material().shader_id) {
|
||||||
|
// specify to use this pipeline
|
||||||
render_pass.set_pipeline(pipeline.get_wgpu_pipeline());
|
render_pass.set_pipeline(pipeline.get_wgpu_pipeline());
|
||||||
|
|
||||||
|
// get the mesh (containing vertices) and the buffers from storage
|
||||||
let mesh = job.mesh();
|
let mesh = job.mesh();
|
||||||
let buffers = self.buffer_storage.get(&job.entity()).unwrap();
|
let buffers = self.buffer_storage.get(&job.entity()).unwrap();
|
||||||
|
|
||||||
|
// Bind the optional texture
|
||||||
if let Some(tex) = buffers.texture_bindgroup.as_ref() {
|
if let Some(tex) = buffers.texture_bindgroup.as_ref() {
|
||||||
render_pass.set_bind_group(0, &tex, &[]);
|
render_pass.set_bind_group(0, &tex, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update transform buffer, and bind to the bind group
|
// Get the bindgroup for job's transform and bind to it using an offset.
|
||||||
self.queue.write_buffer(&self.transform_buffer, 0, bytemuck::cast_slice(&[job.transform().calculate_mat4()]));
|
let transform_indices = buffers.transform_index;
|
||||||
render_pass.set_bind_group(1, &self.transform_bind_group, &[]);
|
let (_, bindgroup) = self.transform_buffers.buffer_bindgroups.get(transform_indices.buffer_index).unwrap();
|
||||||
|
let offset = TransformBuffers::get_offset_for(&self.render_limits, transform_indices);
|
||||||
|
render_pass.set_bind_group(1, bindgroup, &[ offset as u32, ]);
|
||||||
|
|
||||||
// There will always be a camera (hopefully)
|
// Bind camera
|
||||||
render_pass.set_bind_group(2, &self.camera_bind_group, &[]);
|
render_pass.set_bind_group(2, &self.camera_bind_group, &[]);
|
||||||
|
|
||||||
|
// if this mesh uses indices, use them to draw the mesh
|
||||||
if let Some(indices) = buffers.buffer_indices.as_ref() {
|
if let Some(indices) = buffers.buffer_indices.as_ref() {
|
||||||
let indices_len = indices.count().unwrap(); // index buffers will have count, if not thats a bug
|
let indices_len = indices.count().unwrap(); // index buffers will have count, if not thats a bug
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue