diff --git a/crates/renderling-shader/src/pbr.rs b/crates/renderling-shader/src/pbr.rs index 0eabb4ca..8d09f116 100644 --- a/crates/renderling-shader/src/pbr.rs +++ b/crates/renderling-shader/src/pbr.rs @@ -21,7 +21,7 @@ use crate::{ id::Id, math, slab::Slab, - stage::{GpuLight, LightType, LightingModel}, + stage::{light::LightStyle, GpuLight, LightType, LightingModel}, texture::GpuTexture, IsVector, }; @@ -344,7 +344,7 @@ pub fn stage_shade_fragment( prefiltered: Vec3, brdf: Vec2, - lights: Array, + lights: Array, slab: &[u32], ) -> Vec4 { let n = in_norm.alt_norm_or_zero(); @@ -358,19 +358,17 @@ pub fn stage_shade_fragment( // determine the light ray and the radiance match light.light_type { - LightType::END_OF_LIGHTS => { - break; - } - LightType::POINT_LIGHT => { - let frag_to_light = light.position.xyz() - in_pos; + LightStyle::Point => { + let point_light = slab.read(light.into_point_id()); + let frag_to_light = point_light.position - in_pos; let distance = frag_to_light.length(); if distance == 0.0 { continue; } let l = frag_to_light.alt_norm_or_zero(); - let attenuation = light.intensity * 1.0 / (distance * distance); + let attenuation = point_light.intensity * 1.0 / (distance * distance); lo += outgoing_radiance( - light.color, + point_light.color, albedo, attenuation, v, @@ -381,19 +379,20 @@ pub fn stage_shade_fragment( ); } - LightType::SPOT_LIGHT => { - let frag_to_light = light.position.xyz() - in_pos; + LightStyle::Spot => { + let spot_light = slab.read(light.into_spot_id()); + let frag_to_light = spot_light.position - in_pos; let distance = frag_to_light.length(); if distance == 0.0 { continue; } let l = frag_to_light.alt_norm_or_zero(); - let theta: f32 = l.dot(light.direction.xyz().alt_norm_or_zero()); - let epsilon: f32 = light.inner_cutoff - light.outer_cutoff; - let attenuation: f32 = - light.intensity * ((theta - light.outer_cutoff) / epsilon).clamp(0.0, 1.0); + let theta: f32 = l.dot(spot_light.direction.alt_norm_or_zero()); + let epsilon: f32 = spot_light.inner_cutoff - spot_light.outer_cutoff; + let attenuation: f32 = spot_light.intensity + * ((theta - spot_light.outer_cutoff) / epsilon).clamp(0.0, 1.0); lo += outgoing_radiance( - light.color, + spot_light.color, albedo, attenuation, v, @@ -404,11 +403,12 @@ pub fn stage_shade_fragment( ); } - LightType::DIRECTIONAL_LIGHT => { - let l = -light.direction.xyz().alt_norm_or_zero(); - let attenuation = light.intensity; + LightStyle::Directional => { + let dir_light = slab.read(light.into_directional_id()); + let l = -dir_light.direction.alt_norm_or_zero(); + let attenuation = dir_light.intensity; lo += outgoing_radiance( - light.color, + dir_light.color, albedo, attenuation, v, @@ -418,7 +418,6 @@ pub fn stage_shade_fragment( roughness, ); } - _ => {} } } diff --git a/crates/renderling-shader/src/stage.rs b/crates/renderling-shader/src/stage.rs index 5f3853d9..8e533fe7 100644 --- a/crates/renderling-shader/src/stage.rs +++ b/crates/renderling-shader/src/stage.rs @@ -27,6 +27,8 @@ use crate::{ IsMatrix, IsVector, }; +pub mod light; + /// A vertex in a mesh. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] @@ -883,13 +885,25 @@ pub struct Camera { /// This should be the first struct in the stage's slab. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, Slabbed)] pub struct StageLegend { pub atlas_size: UVec2, pub debug_mode: DebugMode, pub has_skybox: bool, pub has_lighting: bool, - pub light_array: Array, + pub light_array: Array, +} + +impl Default for StageLegend { + fn default() -> Self { + Self { + atlas_size: Default::default(), + debug_mode: Default::default(), + has_skybox: Default::default(), + has_lighting: true, + light_array: Default::default(), + } + } } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] diff --git a/crates/renderling-shader/src/stage/id.rs b/crates/renderling-shader/src/stage/id.rs deleted file mode 100644 index fd801ac8..00000000 --- a/crates/renderling-shader/src/stage/id.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Typed identifiers that can also be used as indices. -use core::marker::PhantomData; - -pub const ID_NONE: u32 = u32::MAX; - -/// An identifier. -#[derive(Ord)] -#[repr(transparent)] -#[derive(bytemuck::Pod, bytemuck::Zeroable)] -pub struct Id(pub(crate) u32, PhantomData); - -impl PartialOrd for Id { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} - -impl Copy for Id {} - -impl Clone for Id { - fn clone(&self) -> Self { - Self(self.0.clone(), PhantomData) - } -} - -impl core::hash::Hash for Id { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } -} - -impl PartialEq for Id { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for Id {} - -impl From> for u32 { - fn from(value: Id) -> Self { - value.0 - } -} - -/// `Id::NONE` is the default. -impl Default for Id { - fn default() -> Self { - Id::NONE - } -} - -#[cfg(not(target_arch = "spirv"))] -impl std::fmt::Debug for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&format!("Id<{}>", std::any::type_name::())) - .field(&self.0) - .finish() - } -} - -impl Id { - pub const NONE: Self = Id::new(ID_NONE); - - pub const fn new(i: u32) -> Self { - Id(i, PhantomData) - } - - /// Convert this id into a usize for use as an index. - pub fn index(&self) -> usize { - self.0 as usize - } - - pub fn is_none(&self) -> bool { - // `u32` representing "null" or "none". - self == &Id::NONE - } - - pub fn is_some(&self) -> bool { - !self.is_none() - } -} - -#[cfg(test)] -mod test { - use crate::scene::GpuEntity; - - use super::*; - - #[test] - fn id_size() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>(), - "id is not u32" - ); - } -} diff --git a/crates/renderling-shader/src/stage/light.rs b/crates/renderling-shader/src/stage/light.rs new file mode 100644 index 00000000..5226ed92 --- /dev/null +++ b/crates/renderling-shader/src/stage/light.rs @@ -0,0 +1,197 @@ +//! Stage lighting. +use crate::{self as renderling_shader, id::Id, slab::Slabbed}; +use glam::{Vec3, Vec4}; + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, Slabbed)] +pub struct SpotLight { + pub position: Vec3, + pub direction: Vec3, + pub attenuation: Vec3, + pub inner_cutoff: f32, + pub outer_cutoff: f32, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for SpotLight { + fn default() -> Self { + let white = Vec4::splat(1.0); + let inner_cutoff = core::f32::consts::PI / 3.0; + let outer_cutoff = core::f32::consts::PI / 2.0; + let attenuation = Vec3::new(1.0, 0.014, 0.007); + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = white; + let intensity = 1.0; + + Self { + position: Default::default(), + direction, + attenuation, + inner_cutoff, + outer_cutoff, + color, + intensity, + } + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, Slabbed)] +pub struct DirectionalLight { + pub direction: Vec3, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for DirectionalLight { + fn default() -> Self { + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + direction, + color, + intensity, + } + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, Slabbed)] +pub struct PointLight { + pub position: Vec3, + pub attenuation: Vec3, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for PointLight { + fn default() -> Self { + let attenuation = Vec3::new(1.0, 0.14, 0.07); + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + position: Default::default(), + attenuation, + color, + intensity, + } + } +} + +#[cfg(feature = "gltf")] +pub fn from_gltf_light_kind(kind: gltf::khr_lights_punctual::Kind) -> LightStyle { + match kind { + gltf::khr_lights_punctual::Kind::Directional => LightStyle::Directional, + gltf::khr_lights_punctual::Kind::Point => LightStyle::Point, + gltf::khr_lights_punctual::Kind::Spot { .. } => LightStyle::Spot, + } +} + +#[cfg(feature = "gltf")] +pub fn gltf_light_intensity_units(kind: gltf::khr_lights_punctual::Kind) -> &'static str { + match kind { + gltf::khr_lights_punctual::Kind::Directional => "lux (lm/m^2)", + // sr is "steradian" + _ => "candelas (lm/sr)", + } +} + +#[repr(u32)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone)] +pub enum LightStyle { + Directional = 0, + Point = 1, + Spot = 2, +} + +impl Slabbed for LightStyle { + fn slab_size() -> usize { + 1 + } + + fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { + let mut proxy = 0u32; + let index = proxy.read_slab(index, slab); + match proxy { + 0 => *self = LightStyle::Directional, + 1 => *self = LightStyle::Point, + 2 => *self = LightStyle::Spot, + _ => *self = LightStyle::Directional, + } + index + } + + fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { + let proxy = *self as u32; + proxy.write_slab(index, slab) + } +} + +/// A type-erased light Id that is used as a slab pointer to any light type. +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, Slabbed)] +pub struct Light { + // The type of the light + pub light_type: LightStyle, + // The index of the light in the slab + pub index: u32, +} + +impl Default for Light { + fn default() -> Self { + Self { + light_type: LightStyle::Directional, + index: Id::<()>::NONE.inner(), + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Directional, + index: id.inner(), + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Spot, + index: id.inner(), + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Point, + index: id.inner(), + } + } +} + +impl Light { + pub fn into_directional_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_spot_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_point_id(self) -> Id { + Id::from(self.index) + } +} diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 200a0611..ea68b728 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -40,6 +40,8 @@ //! You can also use the [shaders module](crate::shaders) without renderlings //! and manage your own resources for maximum flexibility. +// TODO: Audit the API and make it more ergonomic/predictable. + mod atlas; pub mod bloom; mod buffer_array; @@ -102,6 +104,8 @@ pub mod shader { pub use renderling_shader::*; } +pub use renderling_shader::stage::{GpuEntityInfo, LightingModel}; + /// Set up the render graph, including: /// * 3d scene objects /// * skybox @@ -212,7 +216,10 @@ mod test { use super::*; use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4, Vec4Swizzles}; use pretty_assertions::assert_eq; - use renderling_shader::stage::{DrawIndirect, GpuEntity, Vertex}; + use renderling_shader::stage::{ + light::*, new_stage_vertex, Camera, GpuEntity, NativeVertexData, RenderUnit, Transform, + Vertex, VertexData, + }; #[test] fn sanity_transmute() { @@ -1108,8 +1115,8 @@ mod test { } #[test] - /// Ensures that the directional light coloring works. - fn scene_cube_directional() { + /// Tests shading with directional light. + fn old_scene_cube_directional() { let mut r = Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); @@ -1172,6 +1179,75 @@ mod test { img_diff::assert_img_eq("scene_cube_directional.png", img); } + #[test] + /// Tests shading with directional light. + fn scene_cube_directional() { + let mut r = + Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); + let stage = r.new_stage(); + stage.configure_graph(&mut r, true); + + let (projection, _) = camera::default_perspective(100.0, 100.0); + let view = Mat4::look_at_rh( + Vec3::new(1.8, 1.8, 1.8), + Vec3::ZERO, + Vec3::new(0.0, 1.0, 0.0), + ); + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + + let red = Vec3::X.extend(1.0); + let green = Vec3::Y.extend(1.0); + let blue = Vec3::Z.extend(1.0); + let dir_red = stage.append(&DirectionalLight { + direction: Vec3::NEG_Y, + color: red, + intensity: 10.0, + }); + let dir_green = stage.append(&DirectionalLight { + direction: Vec3::NEG_X, + color: green, + intensity: 10.0, + }); + let dir_blue = stage.append(&DirectionalLight { + direction: Vec3::NEG_Z, + color: blue, + intensity: 10.0, + }); + let lights = stage.append_array(&[dir_red.into(), dir_green.into(), dir_blue.into()]); + stage.set_lights(lights); + + let material = stage.append(&PbrMaterial::default()); + let vertices = stage.append_array( + &renderling_shader::math::unit_cube() + .into_iter() + .map(|(p, n)| Vertex { + position: p.extend(1.0), + normal: n.extend(0.0), + ..Default::default() + }) + .collect::>(), + ); + let vertex_data = stage.append(&NativeVertexData { + vertices, + material, + ..Default::default() + }); + + let _cube = stage.draw_unit(&RenderUnit { + vertex_data: VertexData::new_native(vertex_data), + vertex_count: vertices.len() as u32, + camera, + ..Default::default() + }); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("scene_cube_directional.png", img); + } + #[test] // Test to make sure that we can reconstruct a normal matrix without using the // inverse transpose of a model matrix, so long as we have the T R S diff --git a/crates/renderling/src/linkage/stage-stage_fragment.spv b/crates/renderling/src/linkage/stage-stage_fragment.spv index f861a93f..03759cd3 100644 Binary files a/crates/renderling/src/linkage/stage-stage_fragment.spv and b/crates/renderling/src/linkage/stage-stage_fragment.spv differ diff --git a/crates/renderling/src/linkage/tonemapping-fragment.spv b/crates/renderling/src/linkage/tonemapping-fragment.spv index ac674150..f7b72dec 100644 Binary files a/crates/renderling/src/linkage/tonemapping-fragment.spv and b/crates/renderling/src/linkage/tonemapping-fragment.spv differ diff --git a/crates/renderling/src/scene.rs b/crates/renderling/src/scene.rs index 54abc872..694a62b8 100644 --- a/crates/renderling/src/scene.rs +++ b/crates/renderling/src/scene.rs @@ -6,7 +6,10 @@ use moongraph::{Move, View, ViewMut}; use renderling_shader::debug::DebugChannel; use snafu::prelude::*; -pub use renderling_shader::{pbr::PbrMaterial, stage::*, texture::*}; +pub use renderling_shader::stage::{ + DrawIndirect, GpuConstants, GpuEntity, GpuLight, GpuToggles, Vertex, +}; +pub use renderling_shader::{pbr::PbrMaterial, texture::*}; use crate::{ bloom::BloomResult, frame::FrameTextureView, hdr::HdrSurface, Atlas, BufferArray, DepthTexture, diff --git a/crates/renderling/src/scene/gltf_support.rs b/crates/renderling/src/scene/gltf_support.rs index 269517be..125a1cd3 100644 --- a/crates/renderling/src/scene/gltf_support.rs +++ b/crates/renderling/src/scene/gltf_support.rs @@ -484,7 +484,7 @@ impl GltfLoader { emissive_factor, emissive_texture, emissive_tex_coord, - lighting_model: LightingModel::PBR_LIGHTING, + lighting_model: crate::shader::stage::LightingModel::PBR_LIGHTING, ..Default::default() } }; diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index 9f2fb6e5..5c38c0e1 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -13,7 +13,7 @@ use renderling_shader::{ debug::DebugMode, id::Id, slab::Slabbed, - stage::{GpuLight, RenderUnit, StageLegend}, + stage::{light::Light, RenderUnit, StageLegend}, texture::GpuTexture, }; use snafu::Snafu; @@ -26,7 +26,6 @@ use crate::{ #[cfg(feature = "gltf")] mod gltf_support; -pub mod light; #[cfg(feature = "gltf")] pub use gltf_support::*; @@ -162,6 +161,15 @@ impl Stage { self } + /// Set the lights to use for shading. + pub fn set_lights(&self, lights: Array) { + let id = Id::>::from(StageLegend::offset_of_light_array()); + // UNWRAP: safe because light array offset is guaranteed to be valid. + self.slab + .write(&self.device, &self.queue, id, &lights) + .unwrap(); + } + /// Set the images to use for the atlas. /// /// Resets the atlas, packing it with the given images and returning a vector of the textures @@ -216,47 +224,6 @@ impl Stage { self } - /// Create a new spot light and return its builder. - pub fn new_spot_light(&self) -> light::GpuSpotLightBuilder { - light::GpuSpotLightBuilder::new(self) - } - - /// Create a new directional light and return its builder. - pub fn new_directional_light(&self) -> light::GpuDirectionalLightBuilder { - light::GpuDirectionalLightBuilder::new(self) - } - - /// Create a new point light and return its builder. - pub fn new_point_light(&self) -> light::GpuPointLightBuilder { - light::GpuPointLightBuilder::new(self) - } - - /// Set the light array. - /// - /// This should be an iterator over the ids of all the lights on the stage. - pub fn set_light_array( - &self, - lights: impl IntoIterator>, - ) -> Array> { - let lights = lights.into_iter().collect::>(); - let light_array = self.append_array(&lights); - let id = Id::>>::from(StageLegend::offset_of_light_array()); - // UNWRAP: safe because we just appended the array, and the light array offset is - // guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &light_array) - .unwrap(); - light_array - } - - /// Set the light array. - /// - /// This should be an iterator over the ids of all the lights on the stage. - pub fn with_light_array(self, lights: impl IntoIterator>) -> Self { - self.set_light_array(lights); - self - } - /// Read all the data from the stage. /// /// This blocks until the GPU buffer is mappable, and then copies the data into a vector. diff --git a/crates/renderling/src/stage/light.rs b/crates/renderling/src/stage/light.rs deleted file mode 100644 index 5a6f57cc..00000000 --- a/crates/renderling/src/stage/light.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Light builders for the stage. -use glam::{Vec3, Vec4}; -use renderling_shader::id::Id; -use renderling_shader::stage::{GpuLight, LightType}; - -use crate::Stage; - -#[cfg(feature = "gltf")] -pub fn from_gltf_light_kind(kind: gltf::khr_lights_punctual::Kind) -> LightType { - match kind { - gltf::khr_lights_punctual::Kind::Directional => LightType::DIRECTIONAL_LIGHT, - gltf::khr_lights_punctual::Kind::Point => LightType::POINT_LIGHT, - gltf::khr_lights_punctual::Kind::Spot { .. } => LightType::SPOT_LIGHT, - } -} - -#[cfg(feature = "gltf")] -pub fn gltf_light_intensity_units(kind: gltf::khr_lights_punctual::Kind) -> &'static str { - match kind { - gltf::khr_lights_punctual::Kind::Directional => "lux (lm/m^2)", - // sr is "steradian" - _ => "candelas (lm/sr)", - } -} - -/// A builder for a spot light. -pub struct GpuSpotLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuSpotLightBuilder<'a> { - pub fn new(stage: &'a Stage) -> GpuSpotLightBuilder<'a> { - let inner = GpuLight { - light_type: LightType::SPOT_LIGHT, - ..Default::default() - }; - let white = Vec4::splat(1.0); - Self { inner, stage } - .with_cutoff(std::f32::consts::PI / 3.0, std::f32::consts::PI / 2.0) - .with_attenuation(1.0, 0.014, 0.007) - .with_direction(Vec3::new(0.0, -1.0, 0.0)) - .with_color(white) - .with_intensity(1.0) - } - - pub fn with_position(mut self, position: impl Into) -> Self { - self.inner.position = position.into().extend(1.0); - self - } - - pub fn with_direction(mut self, direction: impl Into) -> Self { - self.inner.direction = direction.into().extend(1.0); - self - } - - pub fn with_attenuation(mut self, constant: f32, linear: f32, quadratic: f32) -> Self { - self.inner.attenuation = Vec4::new(constant, linear, quadratic, 0.0); - self - } - - pub fn with_cutoff(mut self, inner: f32, outer: f32) -> Self { - self.inner.inner_cutoff = inner; - self.inner.outer_cutoff = outer; - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, i: f32) -> Self { - self.inner.intensity = i; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -} - -/// A builder for a directional light. -/// -/// Directional lights illuminate all geometry from a certain direction, -/// without attenuation. -/// -/// This is like the sun, or the moon. -pub struct GpuDirectionalLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuDirectionalLightBuilder<'a> { - pub fn new(stage: &'a Stage) -> GpuDirectionalLightBuilder<'a> { - let inner = GpuLight { - light_type: LightType::DIRECTIONAL_LIGHT, - ..Default::default() - }; - Self { inner, stage } - .with_direction(Vec3::new(0.0, -1.0, 0.0)) - .with_color(Vec4::splat(1.0)) - .with_intensity(1.0) - } - - pub fn with_direction(mut self, direction: impl Into) -> Self { - self.inner.direction = direction.into().extend(1.0); - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, intensity: f32) -> Self { - self.inner.intensity = intensity; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -} - -pub struct GpuPointLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuPointLightBuilder<'a> { - pub fn new(stage: &Stage) -> GpuPointLightBuilder<'_> { - let inner = GpuLight { - light_type: LightType::POINT_LIGHT, - ..Default::default() - }; - let white = Vec4::splat(1.0); - GpuPointLightBuilder { stage, inner } - .with_attenuation(1.0, 0.14, 0.07) - .with_color(white) - .with_intensity(1.0) - } - - pub fn with_position(mut self, position: impl Into) -> Self { - self.inner.position = position.into().extend(0.0); - self - } - - pub fn with_attenuation(mut self, constant: f32, linear: f32, quadratic: f32) -> Self { - self.inner.attenuation = Vec4::new(constant, linear, quadratic, 0.0); - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, i: f32) -> Self { - self.inner.intensity = i; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -}