1use glam::{Affine3A, Vec3};
20use unlit_ecs::{Bundle, DynamicBundle, Entity, Scope, World};
21use unlit_wgpu::pipeline::LitOptions;
22use unlit3d::bounds::FrustumPlanes;
23use unlit3d::components::{
24 Camera, GpuMaterial, GpuMesh, GpuRenderPipeline, InstanceVisibility, Transform,
25};
26use unlit3d::culling::is_culled;
27use unlit3d::lit::{LitMeshDesc, LitPipelineKey};
28use unlit3d::mesh_source::MeshSource;
29use unlit3d::source::Source;
30
31use unlit_ecs::bundle;
32use unlit3dx::job::Job;
33
34#[derive(Clone, Debug, Default)]
37pub struct OwnedLitMeshDesc {
38 pub positions: Vec<[f32; 3]>,
40 pub uvs: Option<Vec<[f32; 2]>>,
42 pub colors: Option<Vec<[u8; 4]>>,
44 pub indices: Option<Vec<u32>>,
46}
47
48impl OwnedLitMeshDesc {
49 #[must_use]
51 fn borrowed(&self) -> LitMeshDesc<'_> {
52 LitMeshDesc {
53 positions: &self.positions,
54 uvs: self.uvs.as_deref(),
55 colors: self.colors.as_deref(),
56 indices: self.indices.as_deref(),
57 ..Default::default()
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct RenderEntity(pub Entity);
72
73pub(crate) fn mesh_source(world: &World) -> Option<(RenderEntity, LitPipelineKey)> {
85 let mut sources = world.root_scope().query::<(&Source, Entity)>();
86 let entity = sources
87 .iter()
88 .find(|(source, _)| source.as_ref::<MeshSource>().is_ok())
89 .map(|(_, entity)| entity)?;
90 let key = world
91 .root_scope()
92 .query::<&LitPipelineKey>()
93 .iter()
94 .next()
95 .cloned()
96 .unwrap_or_else(|| {
97 LitPipelineKey::new(LitOptions {
98 unlit: true,
99 ..LitOptions::standard()
100 })
101 });
102 Some((RenderEntity(entity), key))
103}
104
105pub(crate) fn allocate_mesh(world: &mut World, desc: &OwnedLitMeshDesc) -> RenderEntity {
112 let Some((source_entity, key)) = mesh_source(world) else {
113 panic!("the render world carries no MeshSource");
114 };
115 let root = world.root_scope();
116 let mesh = root
117 .query_one::<&mut Source>(source_entity.0)
118 .map(|mut source| {
119 source
120 .get()
121 .as_mut::<MeshSource>()
122 .expect("the source carries a MeshSource")
123 .allocate_lit_mesh(&root, &key.options, desc.borrowed())
124 })
125 .expect("the mesh source entity exists");
126 RenderEntity(
129 world
130 .scope_mut(source_entity.0)
131 .expect("the mesh source entity exists")
132 .spawn(bundle![mesh]),
133 )
134}
135
136pub(crate) fn allocate_rgba_material(
146 world: &mut World,
147 size: (u32, u32),
148 pixels: &[[u8; 4]],
149 descriptor: wgpu::SamplerDescriptor<'static>,
150) -> RenderEntity {
151 assert_eq!(
152 pixels.len(),
153 size.0 as usize * size.1 as usize,
154 "the pixels do not fill the texture"
155 );
156 allocate_material(world, descriptor, |source, world| {
157 rgba_texture(&source.device(world), &source.queue(world), size, pixels)
158 })
159}
160
161fn allocate_material(
168 world: &mut World,
169 descriptor: wgpu::SamplerDescriptor<'static>,
170 build_texture: impl FnOnce(&MeshSource, &Scope<'_>) -> wgpu::Texture,
171) -> RenderEntity {
172 let Some((source_entity, key)) = mesh_source(world) else {
173 panic!("the render world carries no MeshSource");
174 };
175 let root = world.root_scope();
176 let material = root
177 .query_one::<&mut Source>(source_entity.0)
178 .map(|mut source| {
179 let source = source
180 .get()
181 .as_mut::<MeshSource>()
182 .expect("the source carries a MeshSource");
183 let texture = build_texture(source, &root);
184 let view = source.register_texture_and_default_view(&root, texture).1;
185 let sampler = source.register_sampler(&root, Some(descriptor));
186 source.allocate_lit_material(&root, &key.options, view, sampler)
187 })
188 .expect("the mesh source entity exists");
189 RenderEntity(
190 world
191 .scope_mut(source_entity.0)
192 .expect("the mesh source entity exists")
193 .spawn(bundle![material]),
194 )
195}
196
197pub(crate) fn instance_of(
204 world: &mut World,
205 mesh: RenderEntity,
206 material: RenderEntity,
207 transform: Transform,
208) -> RenderEntity {
209 let Some((source_entity, key)) = mesh_source(world) else {
210 panic!("the render world carries no MeshSource");
211 };
212 let mesh = world
213 .root_scope()
214 .query_one::<&GpuMesh>(mesh.0)
215 .expect("the mesh asset entity exists")
216 .get()
217 .clone();
218 let material = world
219 .root_scope()
220 .query_one::<&GpuMaterial>(material.0)
221 .expect("the material asset entity exists")
222 .get()
223 .clone();
224 RenderEntity(
227 world
228 .scope_mut(source_entity.0)
229 .expect("the mesh source entity exists")
230 .spawn(Rendered {
231 transform,
232 mesh,
233 material,
234 pipeline: GpuRenderPipeline::new(key),
235 }),
236 )
237}
238
239struct Rendered {
247 transform: Transform,
249 mesh: GpuMesh,
251 material: GpuMaterial,
253 pipeline: GpuRenderPipeline,
255}
256
257impl Bundle for Rendered {
258 fn into_bundle(self) -> DynamicBundle {
259 bundle![
260 self.transform,
261 self.mesh,
262 self.material,
263 self.pipeline,
264 InstanceVisibility::default(),
265 ]
266 }
267}
268
269pub(crate) fn hidden_instance_of(
271 world: &mut World,
272 mesh: RenderEntity,
273 material: RenderEntity,
274 transform: Transform,
275) -> RenderEntity {
276 let instance = instance_of(world, mesh, material, transform);
277 let Some((source, _)) = mesh_source(world) else {
278 return instance;
279 };
280 let Ok(scope) = world.scope(source.0) else {
281 return instance;
282 };
283 let _ = scope
284 .query_one::<&mut InstanceVisibility>(instance.0)
285 .map(|mut visibility| visibility.get().visible = false);
286 instance
287}
288
289#[derive(Clone, Copy, Debug)]
296pub struct BallInstance {
297 pub ball: Entity,
299 pub instance: RenderEntity,
301}
302
303pub(crate) fn sweep(instances: Vec<BallInstance>) -> impl FnOnce(&mut World) -> Vec<BallInstance> {
315 move |world: &mut World| {
316 let frustum = {
317 let mut cameras = world.root_scope().query::<&Camera>();
318 let Some(camera) = cameras.iter().find(|camera| camera.active) else {
319 return Vec::new();
320 };
321 FrustumPlanes::from_clip_from_world(camera.clip_from_world())
322 };
323 let Some((source, _)) = mesh_source(world) else {
324 return Vec::new();
325 };
326 let mut outside = instances;
327 {
328 let Ok(scope) = world.scope(source.0) else {
329 return Vec::new();
330 };
331 outside.retain(|answer| {
334 let Ok(mut mesh) = scope.query_one::<&GpuMesh>(answer.instance.0) else {
335 return false;
336 };
337 let model = scope
338 .query_one::<&Transform>(answer.instance.0)
339 .map_or(Affine3A::IDENTITY, |mut transform| {
340 transform.get().compute_matrix()
341 });
342 is_culled(mesh.get().aabb, &model, &frustum)
343 });
344 }
345 if outside.is_empty() {
346 return outside;
347 }
348 if let Ok(mut scope) = world.scope_mut(source.0) {
349 for answer in &outside {
350 let _ = scope.despawn(answer.instance.0);
351 }
352 }
353 outside
354 }
355}
356
357pub(crate) fn despawn_instances_job(instances: Vec<RenderEntity>) -> Job {
359 Box::new(move |world: &mut World| {
360 let Some((source, _)) = mesh_source(world) else {
361 return;
362 };
363 let Ok(mut scope) = world.scope_mut(source.0) else {
364 return;
365 };
366 for instance in instances {
367 let _ = scope.despawn(instance.0);
368 }
369 })
370}
371
372pub(crate) fn show(
381 world: &mut World,
382 instance: RenderEntity,
383 mesh: RenderEntity,
384 material: RenderEntity,
385 transform: &Transform,
386) -> bool {
387 let Some((source, _)) = mesh_source(world) else {
388 return false;
389 };
390 let Ok(scope) = world.scope(source.0) else {
391 return false;
392 };
393 let Ok(mesh) = scope
394 .query_one::<&GpuMesh>(mesh.0)
395 .map(|mut mesh| mesh.get().clone())
396 else {
397 return false;
398 };
399 let Ok(material) = scope
400 .query_one::<&GpuMaterial>(material.0)
401 .map(|mut material| material.get().clone())
402 else {
403 return false;
404 };
405 scope
406 .query_one::<(
407 &mut GpuMesh,
408 &mut GpuMaterial,
409 &mut Transform,
410 &mut InstanceVisibility,
411 )>(instance.0)
412 .map(|mut row| {
413 let (instance_mesh, instance_material, instance_transform, visibility) = row.get();
414 *instance_mesh = mesh;
415 *instance_material = material;
416 instance_transform.clone_from(transform);
417 visibility.visible = true;
418 true
419 })
420 .unwrap_or(false)
421}
422
423#[must_use]
428pub fn rgba_texture(
429 device: &wgpu::Device,
430 queue: &wgpu::Queue,
431 size: (u32, u32),
432 pixels: &[[u8; 4]],
433) -> wgpu::Texture {
434 assert_eq!(
435 pixels.len(),
436 size.0 as usize * size.1 as usize,
437 "the pixels do not fill the texture"
438 );
439 let texture = device.create_texture(&wgpu::TextureDescriptor {
440 label: Some("rgba_texture"),
441 size: wgpu::Extent3d {
442 width: size.0,
443 height: size.1,
444 depth_or_array_layers: 1,
445 },
446 mip_level_count: 1,
447 sample_count: 1,
448 dimension: wgpu::TextureDimension::D2,
449 format: wgpu::TextureFormat::Rgba8UnormSrgb,
450 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
451 view_formats: &[],
452 });
453 queue.write_texture(
454 texture.as_image_copy(),
455 pixels.as_flattened(),
456 wgpu::TexelCopyBufferLayout {
457 offset: 0,
458 bytes_per_row: Some(size.0 * core::mem::size_of::<[u8; 4]>() as u32),
459 rows_per_image: Some(size.1),
460 },
461 wgpu::Extent3d {
462 width: size.0,
463 height: size.1,
464 depth_or_array_layers: 1,
465 },
466 );
467 texture
468}
469
470pub const WHITE_COLOR: [u8; 4] = [255, 255, 255, 255];
473
474#[must_use]
477pub fn linear_sampler() -> wgpu::SamplerDescriptor<'static> {
478 wgpu::SamplerDescriptor {
479 mag_filter: wgpu::FilterMode::Linear,
480 min_filter: wgpu::FilterMode::Linear,
481 ..Default::default()
482 }
483}
484
485#[must_use]
488pub fn camera_looking_at(
489 size: (u32, u32),
490 eye: Vec3,
491 target: Vec3,
492 fov_y_radians: f32,
493 near: f32,
494) -> Camera {
495 let aspect = size.0 as f32 / size.1.max(1) as f32;
496 Camera {
497 view_from_world: glam::camera::rh::view::look_at_mat4(eye, target, Vec3::Y),
498 clip_from_view: glam::camera::rh::proj::directx::perspective_infinite_reverse(
499 fov_y_radians,
500 aspect,
501 near,
502 ),
503 active: true,
504 }
505}
506
507#[must_use]
519pub fn uv_sphere(radius: f32, slices: usize, stacks: usize) -> OwnedLitMeshDesc {
520 assert!(slices > 0 && stacks > 0, "a sphere needs slices and stacks");
521 let count = (slices + 1) * (stacks + 1);
522 let mut positions = Vec::with_capacity(count);
523 let mut uvs = Vec::with_capacity(count);
524 let mut colors = Vec::with_capacity(count);
525 for stack in 0..=stacks {
526 let v = stack as f32 / stacks as f32;
527 let phi = v * core::f32::consts::PI;
528 let (sin_phi, cos_phi) = phi.sin_cos();
529 for slice in 0..=slices {
530 let u = slice as f32 / slices as f32;
531 let theta = u * core::f32::consts::TAU;
532 let (sin_theta, cos_theta) = theta.sin_cos();
533 positions.push([
534 radius * sin_phi * cos_theta,
535 radius * cos_phi,
536 radius * sin_phi * sin_theta,
537 ]);
538 uvs.push([u, v]);
539 colors.push(WHITE_COLOR);
540 }
541 }
542 let mut indices = Vec::with_capacity(slices * stacks * 6);
543 for stack in 0..stacks {
544 for slice in 0..slices {
545 let row = (stack * (slices + 1) + slice) as u32;
546 let next = row + slices as u32 + 1;
547 indices.extend_from_slice(&[row, row + 1, next, row + 1, next + 1, next]);
548 }
549 }
550 OwnedLitMeshDesc {
551 positions,
552 uvs: Some(uvs),
553 colors: Some(colors),
554 indices: Some(indices),
555 }
556}
557
558const FACES: [([f32; 3], [f32; 3], [f32; 3]); 6] = [
561 ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
562 ([-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
563 ([0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]),
564 ([0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]),
565 ([0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
566 ([0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
567];
568
569#[must_use]
573pub fn cuboid_mesh(half: Vec3) -> OwnedLitMeshDesc {
574 let mut positions = Vec::with_capacity(24);
575 let mut uvs = Vec::with_capacity(24);
576 let mut colors = Vec::with_capacity(24);
577 let mut indices = Vec::with_capacity(36);
578 for (normal, up, right) in FACES {
579 let extent_right = right[0] * half.x + right[1] * half.y + right[2] * half.z;
583 let extent_up = up[0] * half.x + up[1] * half.y + up[2] * half.z;
584 let base = positions.len() as u32;
585 let face = |du: f32, dv: f32| -> [f32; 3] {
586 [
587 normal[0] * half.x + right[0] * du * extent_right + up[0] * dv * extent_up,
588 normal[1] * half.y + right[1] * du * extent_right + up[1] * dv * extent_up,
589 normal[2] * half.z + right[2] * du * extent_right + up[2] * dv * extent_up,
590 ]
591 };
592 let corners = [
593 face(-1.0, -1.0),
594 face(1.0, -1.0),
595 face(1.0, 1.0),
596 face(-1.0, 1.0),
597 ];
598 let corner_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
599 for (position, uv) in corners.into_iter().zip(corner_uvs) {
600 positions.push(position);
601 uvs.push(uv);
602 colors.push(WHITE_COLOR);
603 }
604 let plain = Vec3::from(right)
609 .cross(Vec3::from(up))
610 .dot(Vec3::from(normal))
611 > 0.0;
612 if plain {
613 indices.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]);
614 } else {
615 indices.extend_from_slice(&[base + 2, base + 1, base, base, base + 3, base + 2]);
616 }
617 }
618 OwnedLitMeshDesc {
619 positions,
620 uvs: Some(uvs),
621 colors: Some(colors),
622 indices: Some(indices),
623 }
624}