diff --git a/examples/demos/fps-controller.ts b/examples/demos/fps-controller.ts index e7168bf..c98c098 100644 --- a/examples/demos/fps-controller.ts +++ b/examples/demos/fps-controller.ts @@ -25,6 +25,10 @@ import { FpsCameraBundle, FpsCameraController, FpsCameraPlugin, + VisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, } from '../../src'; import { loadImage } from '../utils/image'; // @ts-ignore @@ -58,6 +62,10 @@ export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { LookTransform, FpsCameraController, Smoother, + VisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, ).write, ); diff --git a/examples/demos/gaussian-splatting.ts b/examples/demos/gaussian-splatting.ts index 496b28f..c38b380 100644 --- a/examples/demos/gaussian-splatting.ts +++ b/examples/demos/gaussian-splatting.ts @@ -22,12 +22,26 @@ import { TonemappingMethod, DebandDither, GaussianSplattingPlugin, + Gaussian, + SphericalHarmonicCoefficients, + GaussianCloud, + VisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, } from '../../src'; import { loadImage } from '../utils/image'; // @ts-ignore import glsl_wgsl_compiler_bg from '../public/glsl_wgsl_compiler_bg.wasm?url'; import { parse } from '@loaders.gl/core'; import { PLYLoader } from '@loaders.gl/ply'; +import { + PositionVisibility, + Rotation, + ScaleOpacity, +} from '../../src/components/gaussian-splatting/f32'; + +const MAX_SIZE_VARIANCE = 5.0; /** * @see https://bevyengine.org/learn/book/getting-started/ecs/ @@ -35,8 +49,7 @@ import { PLYLoader } from '@loaders.gl/ply'; export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { let camera: Entity; - const data = await parse(fetch('/icecream.ply'), PLYLoader); - console.log(data); + const { header, attributes } = await parse(fetch('/icecream.ply'), PLYLoader); class StartUpSystem extends System { commands = new Commands(this); @@ -54,6 +67,10 @@ export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { ColorGrading, Tonemapping, DebandDither, + VisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, ).write, ); @@ -72,6 +89,69 @@ export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { ) .entity.hold(); + const gaussians: Gaussian[] = []; + for (let i = 0; i < (header?.vertexCount || 0); i++) { + const x = attributes.POSITION.value[i * 3 + 0]; + const y = attributes.POSITION.value[i * 3 + 1]; + const z = attributes.POSITION.value[i * 3 + 2]; + const f_dc_0 = attributes.f_dc_0.value[i]; + const f_dc_1 = attributes.f_dc_1.value[i]; + const f_dc_2 = attributes.f_dc_2.value[i]; + const scale_0 = attributes.scale_0.value[i]; + const scale_1 = attributes.scale_1.value[i]; + const scale_2 = attributes.scale_2.value[i]; + const opacity = attributes.opacity.value[i]; + const rot_0 = attributes.rot_0.value[i]; + const rot_1 = attributes.rot_1.value[i]; + const rot_2 = attributes.rot_2.value[i]; + const rot_3 = attributes.rot_3.value[i]; + + const gaussian = new Gaussian({ + rotation: new Rotation({ rotation: [rot_0, rot_1, rot_2, rot_3] }), + position_visibility: new PositionVisibility({ + position: [x, y, z], + }), + scale_opacity: new ScaleOpacity({ + scale: [scale_0, scale_1, scale_2], + opacity, + }), + spherical_harmonic: new SphericalHarmonicCoefficients({ + coefficients: [f_dc_0, f_dc_1, f_dc_2], + }), + }); + gaussians.push(gaussian); + + gaussian.position_visibility.visibility = 1.0; + let mean_scale = + (gaussian.scale_opacity.scale[0] + + gaussian.scale_opacity.scale[1] + + gaussian.scale_opacity.scale[2]) / + 3.0; + for (let i = 0; i < 3; i++) { + gaussian.scale_opacity.scale[i] = Math.exp( + Math.min( + Math.max( + gaussian.scale_opacity.scale[i], + mean_scale - MAX_SIZE_VARIANCE, + ), + mean_scale + MAX_SIZE_VARIANCE, + ), + ); + } + + const norm = Math.sqrt( + new Array(4) + .map((i) => Math.pow(gaussian.rotation.rotation[i], 2.0)) + .reduce((prev, cur) => prev + cur, 0), + ); + for (let i = 0; i < 4; i++) { + gaussian.rotation.rotation[i] /= norm; + } + } + + const cloud = GaussianCloud.from_gaussians(gaussians); + console.log(cloud); + // const mesh = Mesh.from(new Cube(1)); // const material = new Material({ // base_color_texture: baseColorImage, diff --git a/examples/demos/light.ts b/examples/demos/light.ts index a701182..8f88843 100644 --- a/examples/demos/light.ts +++ b/examples/demos/light.ts @@ -32,6 +32,11 @@ import { AmbientLight, CascadeShadowConfigBuilder, Plane, + CascadesVisibleEntities, + Visibility, + VisibleEntities, + InheritedVisibility, + ViewVisibility, } from '../../src'; import { loadImage } from '../utils/image'; // @ts-ignore @@ -72,6 +77,11 @@ export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { CascadesFrusta, Cascades, CascadeShadowConfig, + CascadesVisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, + VisibleEntities, ).write, ); diff --git a/examples/demos/orbit-controller.ts b/examples/demos/orbit-controller.ts index a28a3a2..2d36a1e 100644 --- a/examples/demos/orbit-controller.ts +++ b/examples/demos/orbit-controller.ts @@ -30,6 +30,10 @@ import { OrbitCameraBundle, OrbitCameraController, OrbitCameraPlugin, + VisibleEntities, + Visibility, + InheritedVisibility, + ViewVisibility, } from '../../src'; import { loadImage } from '../utils/image'; // @ts-ignore @@ -65,6 +69,10 @@ export async function render($canvas: HTMLCanvasElement, gui: lil.GUI) { LookTransform, OrbitCameraController, Smoother, + Visibility, + VisibleEntities, + InheritedVisibility, + ViewVisibility, ).write, ); diff --git a/src/components/camera/Camera3dBundle.ts b/src/components/camera/Camera3dBundle.ts index 27a4bde..b805f81 100644 --- a/src/components/camera/Camera3dBundle.ts +++ b/src/components/camera/Camera3dBundle.ts @@ -6,30 +6,25 @@ import { Bundle } from '../Bundle'; import { ComputedCameraValues } from './ComputedCameraValues'; import { Frustum } from '../primitive/Frustum'; import { Tonemapping } from '../pipeline'; -import { ColorGrading, DebandDither } from '../render'; +import { ColorGrading, DebandDither, VisibleEntities } from '../render'; export class Camera3dBundle extends Bundle { camera: Camera; - computed: ComputedCameraValues; - projection: Perspective | Orthographic; - + visible_entities: VisibleEntities; + frustum: Frustum; transform: Transform; - tonemapping: Tonemapping; - dither: DebandDither; - color_grading: ColorGrading; - frustum: Frustum; - constructor( options?: Partial<{ camera: Camera; computed: ComputedCameraValues; projection: Perspective | Orthographic; + visible_entities: VisibleEntities; transform: Transform; global_transform: GlobalTransform; tonemapping: Tonemapping; @@ -45,6 +40,7 @@ export class Camera3dBundle extends Bundle { computed = new ComputedCameraValues(), transform, projection = new Perspective(), + visible_entities = new VisibleEntities(), tonemapping = new Tonemapping(), color_grading = new ColorGrading(), dither = new DebandDither(), @@ -55,6 +51,7 @@ export class Camera3dBundle extends Bundle { this.computed = computed; this.transform = transform; this.projection = projection; + this.visible_entities = visible_entities; this.tonemapping = tonemapping; this.color_grading = color_grading; this.dither = dither; diff --git a/src/components/light/CascadesVisibleEntities.ts b/src/components/light/CascadesVisibleEntities.ts new file mode 100644 index 0000000..f2c29ea --- /dev/null +++ b/src/components/light/CascadesVisibleEntities.ts @@ -0,0 +1,10 @@ +import { Entity, field } from '@lastolivegames/becsy'; +import { VisibleEntities } from '../render'; + +export class CascadesVisibleEntities { + @field.object declare entities: Map; + + constructor() { + this.entities = new Map(); + } +} diff --git a/src/components/light/DirectionalLightBundle.ts b/src/components/light/DirectionalLightBundle.ts index cf9e0e9..c2664db 100644 --- a/src/components/light/DirectionalLightBundle.ts +++ b/src/components/light/DirectionalLightBundle.ts @@ -1,8 +1,10 @@ import { Bundle } from '../Bundle'; import { CascadesFrusta } from '../primitive'; +import { InheritedVisibility, ViewVisibility, Visibility } from '../render'; import { Transform } from '../transform'; import { CascadeShadowConfig } from './CascadeShadowConfig'; import { Cascades } from './Cascades'; +import { CascadesVisibleEntities } from './CascadesVisibleEntities'; import { DirectionalLight } from './DirectionalLight'; export class DirectionalLightBundle extends Bundle { @@ -10,14 +12,11 @@ export class DirectionalLightBundle extends Bundle { frusta: CascadesFrusta; cascades: Cascades; cascade_shadow_config: CascadeShadowConfig; - // pub visible_entities: CascadesVisibleEntities, + visible_entities: CascadesVisibleEntities; transform: Transform; - // /// Enables or disables the light - // pub visibility: Visibility, - // /// Inherited visibility of an entity. - // pub inherited_visibility: InheritedVisibility, - // /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - // pub view_visibility: ViewVisibility, + visibility: Visibility; + inherited_visibility: InheritedVisibility; + view_visibility: ViewVisibility; constructor( options?: Partial<{ @@ -25,11 +24,11 @@ export class DirectionalLightBundle extends Bundle { frusta: CascadesFrusta; cascades: Cascades; cascade_shadow_config: CascadeShadowConfig; - // visible_entities: CascadesVisibleEntities, + visible_entities: CascadesVisibleEntities; transform: Transform; - // visibility: Visibility, - // inherited_visibility: InheritedVisibility, - // view_visibility: ViewVisibility, + visibility: Visibility; + inherited_visibility: InheritedVisibility; + view_visibility: ViewVisibility; }>, ) { super(); @@ -39,21 +38,21 @@ export class DirectionalLightBundle extends Bundle { frusta = new CascadesFrusta(), cascades = new Cascades(), cascade_shadow_config = new CascadeShadowConfig(), - // visible_entities = new CascadesVisibleEntities(), + visible_entities = new CascadesVisibleEntities(), transform, - // visibility = new Visibility(), - // inherited_visibility = new InheritedVisibility(), - // view_visibility = new ViewVisibility(), + visibility = new Visibility(), + inherited_visibility = new InheritedVisibility(), + view_visibility = new ViewVisibility(), } = options || {}; this.directional_light = directional_light; this.frusta = frusta; this.cascades = cascades; this.cascade_shadow_config = cascade_shadow_config; - // this.visible_entities = visible_entities; + this.visible_entities = visible_entities; this.transform = transform; - // this.visibility = visibility; - // this.inherited_visibility = inherited_visibility; - // this.view_visibility = view_visibility; + this.visibility = visibility; + this.inherited_visibility = inherited_visibility; + this.view_visibility = view_visibility; } } diff --git a/src/components/light/index.ts b/src/components/light/index.ts index 230c9f0..c5ed415 100644 --- a/src/components/light/index.ts +++ b/src/components/light/index.ts @@ -7,4 +7,5 @@ export * from './DirectionalLightBundle'; export * from './Clusters'; export * from './ClusterConfig'; export * from './CascadeShadowConfig'; +export * from './CascadesVisibleEntities'; export * from './Cascades'; diff --git a/src/components/pbr/PbrBundle.ts b/src/components/pbr/PbrBundle.ts index e1349a7..3ebb8b0 100644 --- a/src/components/pbr/PbrBundle.ts +++ b/src/components/pbr/PbrBundle.ts @@ -40,9 +40,9 @@ export class PbrBundle extends Bundle { mesh, material, transform, - visibility, - inherited_visibility, - view_visibility, + visibility = new Visibility(), + inherited_visibility = new InheritedVisibility(), + view_visibility = new ViewVisibility(), } = options || {}; this.mesh = mesh; this.material = material; diff --git a/src/components/primitive/CascadesFrusta.ts b/src/components/primitive/CascadesFrusta.ts index a8ced6e..5138bbc 100644 --- a/src/components/primitive/CascadesFrusta.ts +++ b/src/components/primitive/CascadesFrusta.ts @@ -3,4 +3,8 @@ import { Frustum } from './Frustum'; export class CascadesFrusta { @field.object declare frusta: Map; + + constructor() { + this.frusta = new Map(); + } } diff --git a/src/components/render/RenderLayers.ts b/src/components/render/RenderLayers.ts index 4939407..778be26 100644 --- a/src/components/render/RenderLayers.ts +++ b/src/components/render/RenderLayers.ts @@ -1,3 +1,5 @@ +import { field } from '@lastolivegames/becsy'; + /// An identifier for a rendering layer. export type Layer = number; @@ -45,7 +47,11 @@ export class RenderLayers { // layers.iter().copied().collect() // } - constructor(private layer = 0) {} + @field.uint8 declare layer: number; + + constructor(layer = 0) { + this.layer = layer; + } /** * Add the given layer. diff --git a/src/components/transform/GlobalTransform.ts b/src/components/transform/GlobalTransform.ts index 032c5ac..cd3b30c 100644 --- a/src/components/transform/GlobalTransform.ts +++ b/src/components/transform/GlobalTransform.ts @@ -13,7 +13,7 @@ import { Transform } from './Transform'; export class GlobalTransform extends Affine3 { static copy(rhs: GlobalTransform) { return new GlobalTransform( - Mat3.copy(rhs.affine()), + Mat3.copy(rhs.matrix3), Vec3.copy(rhs.translation), ); } @@ -43,7 +43,7 @@ export class GlobalTransform extends Affine3 { * Returns the 3d affine transformation matrix as an [`Affine3A`]. */ affine() { - return this.matrix3; + return this; } from(transform: Transform) { @@ -51,4 +51,8 @@ export class GlobalTransform extends Affine3 { this.matrix3 = matrix3; this.translation = translation; } + + radius_vec3(extents: Vec3) { + return this.matrix3.mul_vec3(extents)._length(); + } } diff --git a/src/index.ts b/src/index.ts index c79e4c2..32cf88c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { CorePipeline, InputPlugin, RenderPlugin, + PbrPlugin, } from './plugins'; export { @@ -38,6 +39,7 @@ export const DefaultPlugins: PluginType[] = [ HierarchyPlugin, RenderPlugin, CorePipeline, + PbrPlugin, RendererPlugin, InputPlugin, ]; diff --git a/src/plugins/Renderer.ts b/src/plugins/Renderer.ts index f749cf5..8b94d28 100644 --- a/src/plugins/Renderer.ts +++ b/src/plugins/Renderer.ts @@ -11,6 +11,8 @@ import { PreUpdate, PreStartUp, RenderResource, + PostUpdate, + Last, } from '../systems'; import { Mesh } from '../meshes'; import { Material } from '../components'; @@ -23,6 +25,6 @@ export class RendererPlugin implements Plugin { app.add_systems(PreStartUp, RenderResource); app.add_systems(PreUpdate, PrepareViewUniforms); // app.add_systems(Update, ExtractMeshes); - app.add_systems(Update, MeshPipeline); + app.add_systems(Last, MeshPipeline); } } diff --git a/src/plugins/Transform.ts b/src/plugins/Transform.ts index 17963af..6327433 100644 --- a/src/plugins/Transform.ts +++ b/src/plugins/Transform.ts @@ -9,17 +9,21 @@ import { PreUpdate, PostStartup, PostUpdate, + Last, } from '../systems'; /** * Set enum for the systems relating to transform propagation */ -export namespace TransformSystems { +export namespace TransformSystem { /** * Propagates changes in transform to children's [`GlobalTransform`] */ export const TransformPropagate = System.group(); } +TransformSystem.TransformPropagate.schedule((s) => + s.after(PostUpdate).before(Last), +); export class TransformPlugin implements Plugin { async build(app: App) { @@ -29,7 +33,11 @@ export class TransformPlugin implements Plugin { app.add_systems(PostStartup, ValidParentCheck); // add transform systems to startup so the first update is "correct" - app.add_systems(PreUpdate, SyncSimpleTransforms, PropagateTransforms); + app.add_systems( + TransformSystem.TransformPropagate, + SyncSimpleTransforms, + PropagateTransforms, + ); // app.add_systems(PostStartup, SyncSimpleTransforms, PropagateTransforms); // app.add_systems(PostUpdate, SyncSimpleTransforms, PropagateTransforms); } diff --git a/src/plugins/core/Core3d.ts b/src/plugins/core/Core3d.ts index fdeaf80..c1b4de3 100644 --- a/src/plugins/core/Core3d.ts +++ b/src/plugins/core/Core3d.ts @@ -1,75 +1,8 @@ -import { component, system } from '@lastolivegames/becsy'; import { App } from '../../App'; import { Plugin } from '../../Plugin'; -import { FogPlugin } from '../Fog'; import { SkyboxPlugin } from '../Skybox'; -import { - AmbientLight, - DirectionalLight, - ExtractedDirectionalLight, - PointLight, - Cascade, - CascadeShadowConfig, - Cascades, - ClusterConfig, - Clusters, - CascadesFrusta, - DirectionalLightShadowMap, -} from '../../components'; -import { - ExtractLights, - PreUpdate, - PrepareLights, - PostUpdate, - AddClusters, - AssignLightsToClusters, - ClearDirectionalLightCascades, - BuildDirectionalLightCascades, - Update, - UpdateDirectionalLightFrusta, -} from '../../systems'; - -/** - * PREPASS, - DEFERRED_PREPASS, - COPY_DEFERRED_LIGHTING_ID, - END_PREPASSES, - START_MAIN_PASS, - MAIN_OPAQUE_PASS, - MAIN_TRANSMISSIVE_PASS, - MAIN_TRANSPARENT_PASS, - END_MAIN_PASS, - TONEMAPPING, - END_MAIN_PASS_POST_PROCESSING, - UPSCALING, - */ export class Core3dPlugin implements Plugin { async build(app: App) { - component(ClusterConfig); - component(Clusters); - component(Cascade); - component(Cascades); - component(CascadeShadowConfig); - component(AmbientLight); - component(PointLight); - component(DirectionalLight); - component(ExtractedDirectionalLight); - - app.init_resource( - DirectionalLightShadowMap, - new DirectionalLightShadowMap(), - ); - - await new FogPlugin().build(app); await new SkyboxPlugin().build(app); - - app.add_systems(PreUpdate, ExtractLights); - app.add_systems(PreUpdate, PrepareLights); - app.add_systems(PostUpdate, AddClusters); - app.add_systems(PostUpdate, AssignLightsToClusters); - app.add_systems(PostUpdate, ClearDirectionalLightCascades); - app.add_systems(PostUpdate, BuildDirectionalLightCascades); - system((s) => s.afterWritersOf(Cascades))(BuildDirectionalLightCascades); - app.add_systems(PostUpdate, UpdateDirectionalLightFrusta); } } diff --git a/src/plugins/core/CorePipeline.ts b/src/plugins/core/CorePipeline.ts index 9881d32..ad6c304 100644 --- a/src/plugins/core/CorePipeline.ts +++ b/src/plugins/core/CorePipeline.ts @@ -11,6 +11,7 @@ import { /** * Provides a core render pipeline. + * @see bevy_core_pipeline */ export class CorePipeline implements Plugin { async build(app: App) { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 4093ac2..07e1e72 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -7,4 +7,5 @@ export { HierarchyPlugin } from './Hierarchy'; export { InputPlugin } from './Input'; export * from './core'; export * from './render'; +export * from './pbr'; export * from './gaussian-splatting'; diff --git a/src/plugins/pbr/Light.ts b/src/plugins/pbr/Light.ts new file mode 100644 index 0000000..8a7a513 --- /dev/null +++ b/src/plugins/pbr/Light.ts @@ -0,0 +1,27 @@ +import { System } from '@lastolivegames/becsy'; +import { Last, PostUpdate } from '../../systems'; + +export namespace SimulationLightSystems { + export const AddClusters = System.group(); + export const AddClustersFlush = System.group(); + export const AssignLightsToClusters = System.group(); + export const UpdateDirectionalLightCascades = System.group(); + export const UpdateLightFrusta = System.group(); + export const CheckLightVisibility = System.group(); +} + +SimulationLightSystems.AddClusters.schedule((s) => + s.before(SimulationLightSystems.AddClustersFlush).after(PostUpdate), +); +SimulationLightSystems.AddClustersFlush.schedule((s) => + s.before(SimulationLightSystems.AssignLightsToClusters), +); +SimulationLightSystems.AssignLightsToClusters.schedule((s) => + s.before(SimulationLightSystems.UpdateDirectionalLightCascades), +); +SimulationLightSystems.UpdateDirectionalLightCascades.schedule((s) => + s.before(SimulationLightSystems.UpdateLightFrusta), +); +SimulationLightSystems.UpdateLightFrusta.schedule((s) => + s.before(SimulationLightSystems.CheckLightVisibility).before(Last), +); diff --git a/src/plugins/pbr/Pbr.ts b/src/plugins/pbr/Pbr.ts new file mode 100644 index 0000000..8c9da0e --- /dev/null +++ b/src/plugins/pbr/Pbr.ts @@ -0,0 +1,94 @@ +import { System, component, system } from '@lastolivegames/becsy'; +import { App } from '../../App'; +import { Plugin } from '../../Plugin'; +import { FogPlugin } from '../Fog'; +import { + AmbientLight, + DirectionalLight, + ExtractedDirectionalLight, + PointLight, + Cascade, + CascadeShadowConfig, + CascadesVisibleEntities, + Cascades, + ClusterConfig, + Clusters, + CascadesFrusta, + DirectionalLightShadowMap, +} from '../../components'; +import { + ExtractLights, + PreUpdate, + PrepareLights, + PostUpdate, + AddClusters, + AssignLightsToClusters, + ClearDirectionalLightCascades, + BuildDirectionalLightCascades, + Update, + UpdateDirectionalLightFrusta, + CheckLightMeshVisibility, +} from '../../systems'; +import { SimulationLightSystems } from './Light'; + +/** + * PREPASS, + DEFERRED_PREPASS, + COPY_DEFERRED_LIGHTING_ID, + END_PREPASSES, + START_MAIN_PASS, + MAIN_OPAQUE_PASS, + MAIN_TRANSMISSIVE_PASS, + MAIN_TRANSPARENT_PASS, + END_MAIN_PASS, + TONEMAPPING, + END_MAIN_PASS_POST_PROCESSING, + UPSCALING, + */ +export class PbrPlugin implements Plugin { + async build(app: App) { + component(ClusterConfig); + component(Clusters); + component(Cascade); + component(Cascades); + component(CascadeShadowConfig); + component(CascadesVisibleEntities); + component(AmbientLight); + component(PointLight); + component(DirectionalLight); + component(ExtractedDirectionalLight); + + app.init_resource( + DirectionalLightShadowMap, + new DirectionalLightShadowMap(), + ); + + await new FogPlugin().build(app); + + app.add_systems(PreUpdate, ExtractLights); + app.add_systems(PreUpdate, PrepareLights); + app.add_systems(SimulationLightSystems.AddClusters, AddClusters); + app.add_systems( + SimulationLightSystems.AddClustersFlush, + class AddClustersFlushPlaceHolder extends System {}, + ); + app.add_systems( + SimulationLightSystems.AssignLightsToClusters, + AssignLightsToClusters, + ); + + app.add_systems( + SimulationLightSystems.UpdateDirectionalLightCascades, + ClearDirectionalLightCascades, + BuildDirectionalLightCascades, + ); + app.add_systems( + SimulationLightSystems.UpdateLightFrusta, + UpdateDirectionalLightFrusta, + ); + app.add_systems( + SimulationLightSystems.CheckLightVisibility, + CheckLightMeshVisibility, + ); + } +} diff --git a/src/plugins/pbr/index.ts b/src/plugins/pbr/index.ts new file mode 100644 index 0000000..42703de --- /dev/null +++ b/src/plugins/pbr/index.ts @@ -0,0 +1,2 @@ +export * from './Light'; +export * from './Pbr'; diff --git a/src/plugins/render/Camera.ts b/src/plugins/render/Camera.ts index 82e167e..313d11e 100644 --- a/src/plugins/render/Camera.ts +++ b/src/plugins/render/Camera.ts @@ -13,6 +13,7 @@ import { } from '../../components'; import { CameraSystem, + ExtractCameras, LookTransformSystem, PreUpdate, Update, @@ -21,14 +22,15 @@ import { export class CameraPlugin implements Plugin { async build(app: App) { component(Camera); - component(ComputedCameraValues); component(Viewport); + component(ComputedCameraValues); component(Projection)(Perspective); component(Projection)(Orthographic); component(LookTransform); component(Smoother); app.add_systems(PreUpdate, CameraSystem); + app.add_systems(PreUpdate, ExtractCameras); app.add_systems(Update, LookTransformSystem); system((s) => s.afterWritersOf(Camera))(CameraSystem); diff --git a/src/plugins/render/Visibility.ts b/src/plugins/render/Visibility.ts index 4b9b615..c359b00 100644 --- a/src/plugins/render/Visibility.ts +++ b/src/plugins/render/Visibility.ts @@ -10,7 +10,7 @@ import { UpdateFrusta, VisibilityPropagate, } from '../../systems'; -import { TransformSystems } from '../Transform'; +import { TransformSystem } from '../Transform'; export namespace VisibilitySystems { /// Label for the [`calculate_bounds`] and `calculate_bounds_2d` systems, diff --git a/src/shaders/gaussian-splatting/bindings.ts b/src/shaders/gaussian-splatting/bindings.ts new file mode 100644 index 0000000..172e91d --- /dev/null +++ b/src/shaders/gaussian-splatting/bindings.ts @@ -0,0 +1,17 @@ +export default /* wgsl */ ` +#define_import_path gaussian_splatting::bindings + +#import render::globals::Globals +#import render::view::View + +@group(0) @binding(0) var view: View; +// @group(0) @binding(1) var globals: Globals; + +struct GaussianUniforms { + global_transform: mat4x4, + global_scale: f32, + count: u32, + count_root_ceil: u32, +}; +@group(1) @binding(0) var gaussian_uniforms: GaussianUniforms; +`; diff --git a/src/shaders/gaussian-splatting/gaussian.ts b/src/shaders/gaussian-splatting/gaussian.ts new file mode 100644 index 0000000..f5868ad --- /dev/null +++ b/src/shaders/gaussian-splatting/gaussian.ts @@ -0,0 +1,339 @@ +export default /* wgsl */ ` +#import gaussian_splatting::bindings::{ + view, + globals, + gaussian_uniforms, + sorting_pass_index, + sorting, + draw_indirect, + input_entries, + output_entries, + Entry, +} + +struct GaussianVertexOutput { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) color: vec4, + @location(1) @interpolate(flat) conic: vec3, + @location(2) @interpolate(linear) uv: vec2, + @location(3) @interpolate(linear) major_minor: vec2, +}; + +// https://github.com/cvlab-epfl/gaussian-splatting-web/blob/905b3c0fb8961e42c79ef97e64609e82383ca1c2/src/shaders.ts#L185 +// TODO: precompute +fn compute_cov3d(scale: vec3, rotation: vec4) -> array { + let S = mat3x3( + scale.x * gaussian_uniforms.global_scale, 0.0, 0.0, + 0.0, scale.y * gaussian_uniforms.global_scale, 0.0, + 0.0, 0.0, scale.z * gaussian_uniforms.global_scale, + ); + + let r = rotation.x; + let x = rotation.y; + let y = rotation.z; + let z = rotation.w; + + let R = mat3x3( + 1.0 - 2.0 * (y * y + z * z), + 2.0 * (x * y - r * z), + 2.0 * (x * z + r * y), + + 2.0 * (x * y + r * z), + 1.0 - 2.0 * (x * x + z * z), + 2.0 * (y * z - r * x), + + 2.0 * (x * z - r * y), + 2.0 * (y * z + r * x), + 1.0 - 2.0 * (x * x + y * y), + ); + + let M = S * R; + let Sigma = transpose(M) * M; + + return array( + Sigma[0][0], + Sigma[0][1], + Sigma[0][2], + Sigma[1][1], + Sigma[1][2], + Sigma[2][2], + ); +} + +fn compute_cov2d( + position: vec3, + index: u32, +) -> vec3 { +#ifdef PRECOMPUTE_COVARIANCE_3D + let cov3d = get_cov3d(index); +#else + let rotation = get_rotation(index); + let scale = get_scale(index); + + let cov3d = compute_cov3d(scale, rotation); +#endif + + let Vrk = mat3x3( + cov3d[0], cov3d[1], cov3d[2], + cov3d[1], cov3d[3], cov3d[4], + cov3d[2], cov3d[4], cov3d[5], + ); + + var t = view.inverse_view * vec4(position, 1.0); + + let focal = vec2( + view.projection.x.x * view.viewport.z, + view.projection.y.y * view.viewport.w, + ); + + let s = 1.0 / (t.z * t.z); + let J = mat3x3( + focal.x / t.z, 0.0, -(focal.x * t.x) * s, + 0.0, -focal.y / t.z, (focal.y * t.y) * s, + 0.0, 0.0, 0.0, + ); + + let W = transpose( + mat3x3( + view.inverse_view.x.xyz, + view.inverse_view.y.xyz, + view.inverse_view.z.xyz, + ) + ); + + let T = W * J; + + var cov = transpose(T) * transpose(Vrk) * T; + cov[0][0] += 0.3f; + cov[1][1] += 0.3f; + + return vec3(cov[0][0], cov[0][1], cov[1][1]); +} + +fn get_bounding_box( + cov2d: vec3, + direction: vec2, +) -> vec4 { + // return vec4(offset, uv); + + let det = cov2d.x * cov2d.z - cov2d.y * cov2d.y; + let trace = cov2d.x + cov2d.z; + let mid = 0.5 * trace; + let discriminant = max(0.0, mid * mid - det); + + let term = sqrt(discriminant); + + let lambda1 = mid + term; + let lambda2 = max(mid - term, 0.0); + + let x_axis_length = sqrt(lambda1); + let y_axis_length = sqrt(lambda2); + + +#ifdef USE_AABB + let radius_px = 3.5 * max(x_axis_length, y_axis_length); + let radius_ndc = vec2( + radius_px / view.viewport.zw, + ); + + return vec4( + radius_ndc * direction, + radius_px * direction, + ); +#endif + +#ifdef USE_OBB + + let a = (cov2d.x - cov2d.z) * (cov2d.x - cov2d.z); + let b = sqrt(a + 4.0 * cov2d.y * cov2d.y); + let major_radius = sqrt((cov2d.x + cov2d.z + b) * 0.5); + let minor_radius = sqrt((cov2d.x + cov2d.z - b) * 0.5); + + let bounds = 3.5 * vec2( + major_radius, + minor_radius, + ); + + let eigvec1 = normalize(vec2( + -cov2d.y, + lambda1 - cov2d.x, + )); + let eigvec2 = vec2( + eigvec1.y, + -eigvec1.x + ); + + let rotation_matrix = transpose( + mat2x2( + eigvec1, + eigvec2, + ) + ); + + let scaled_vertex = direction * bounds; + let rotated_vertex = scaled_vertex * rotation_matrix; + + let scaling_factor = 1.0 / view.viewport.zw; + let ndc_vertex = rotated_vertex * scaling_factor; + + return vec4( + ndc_vertex, + rotated_vertex, + ); +#endif +} + + +@vertex +fn vs_points( + @builtin(instance_index) instance_index: u32, + @builtin(vertex_index) vertex_index: u32, +) -> GaussianVertexOutput { + var output: GaussianVertexOutput; + + let entry = get_entry(instance_index); + let splat_index = entry.value; + + var discard_quad = false; + + discard_quad |= entry.key == 0xFFFFFFFFu; // || splat_index == 0u; + + let position = vec4(get_position(splat_index), 1.0); + + let transformed_position = (gaussian_uniforms.global_transform * position).xyz; + let projected_position = world_to_clip(transformed_position); + + discard_quad |= !in_frustum(projected_position.xyz); + +#ifdef DRAW_SELECTED + discard_quad |= get_visibility(splat_index) < 0.5; +#endif + + if (discard_quad) { + output.color = vec4(0.0, 0.0, 0.0, 0.0); + output.position = vec4(0.0, 0.0, 0.0, 0.0); + return output; + } + + var quad_vertices = array, 4>( + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2( 1.0, -1.0), + vec2( 1.0, 1.0), + ); + + let quad_index = vertex_index % 4u; + let quad_offset = quad_vertices[quad_index]; + + let ray_direction = normalize(transformed_position - view.world_position); + + var rgb = vec3(0.0); + +#ifdef VISUALIZE_DEPTH + let first_position = vec4(get_position(get_entry(1u).value), 1.0); + let last_position = vec4(get_position(get_entry(gaussian_uniforms.count - 1u).value), 1.0); + + let min_position = (gaussian_uniforms.global_transform * first_position).xyz; + let max_position = (gaussian_uniforms.global_transform * last_position).xyz; + + let camera_position = view.world_position; + + let min_distance = length(min_position - camera_position); + let max_distance = length(max_position - camera_position); + + let depth = length(transformed_position - camera_position); + rgb = depth_to_rgb( + depth, + min_distance, + max_distance, + ); +#else + rgb = get_color(splat_index, ray_direction); +#endif + + // TODO: verify color benefit for ray_direction computed at quad verticies instead of gaussian center (same as current complexity) + output.color = vec4( + rgb, + get_opacity(splat_index), + ); + +#ifdef HIGHLIGHT_SELECTED + if (get_visibility(splat_index) > 0.5) { + output.color = vec4(0.3, 1.0, 0.1, 1.0); + } +#endif + + let cov2d = compute_cov2d(transformed_position, splat_index); + +#ifdef USE_AABB + let det = cov2d.x * cov2d.z - cov2d.y * cov2d.y; + let det_inv = 1.0 / det; + let conic = vec3( + cov2d.z * det_inv, + -cov2d.y * det_inv, + cov2d.x * det_inv + ); + output.conic = conic; +#endif + + let bb = get_bounding_box( + cov2d, + quad_offset, + ); + + output.uv = quad_offset; + output.major_minor = bb.zw; + output.position = vec4( + projected_position.xy + bb.xy, + projected_position.zw + ); + + return output; +} + +@fragment +fn fs_main(input: GaussianVertexOutput) -> @location(0) vec4 { +#ifdef USE_AABB + let d = -input.major_minor; + let conic = input.conic; + let power = -0.5 * (conic.x * d.x * d.x + conic.z * d.y * d.y) + conic.y * d.x * d.y; + + if (power > 0.0) { + discard; + } +#endif + +#ifdef USE_OBB + let sigma = 1.0 / 3.5; + let sigma_squared = 2.0 * sigma * sigma; + let distance_squared = dot(input.uv, input.uv); + + let power = -distance_squared / sigma_squared; + + if (distance_squared > 3.5 * 3.5) { + discard; + } +#endif + +#ifdef VISUALIZE_BOUNDING_BOX + let uv = (input.uv + 1.0) / 2.0; + let edge_width = 0.08; + if ( + (uv.x < edge_width || uv.x > 1.0 - edge_width) || + (uv.y < edge_width || uv.y > 1.0 - edge_width) + ) { + return vec4(0.3, 1.0, 0.1, 1.0); + } +#endif + + let alpha = exp(power); + let final_alpha = alpha * input.color.a; + + // TODO: round final_alpha to terminate depth test? + + return vec4( + input.color.rgb * final_alpha, + final_alpha, + ); +} +`; diff --git a/src/systems/MeshPipeline.ts b/src/systems/MeshPipeline.ts index 7705ae9..68fb5b7 100644 --- a/src/systems/MeshPipeline.ts +++ b/src/systems/MeshPipeline.ts @@ -15,7 +15,6 @@ import { Mesh } from '../meshes'; import { PrepareViewUniforms } from './PrepareViewUniforms'; import { PrepareFog } from './PrepareFog'; import { RenderResource } from './RenderResource'; -import { ExtractMeshes } from './ExtractMeshes'; import { OpaqueNode } from './nodes/Opaque'; import { PipelineNode } from './nodes/PipelineNode'; import { PrepareLights } from './PrepareLights'; diff --git a/src/systems/camera/Camera.ts b/src/systems/camera/Camera.ts index 6e181de..c535a51 100644 --- a/src/systems/camera/Camera.ts +++ b/src/systems/camera/Camera.ts @@ -8,27 +8,6 @@ import { } from '../../components'; import { Vec2 } from '../../math'; -// export class ExtractCameras extends System { -// private cameras = this.query( -// (q) => q.added.with(Camera).trackWrites, -// ); - -// execute(): void { -// const { canvas } = this.appConfig; -// const { width, height } = canvas; -// this.cameras.added.forEach((entity) => { -// const camera = entity.read(Camera); -// if (!camera.is_active) { -// return; -// } - -// entity.add(ExtractedCamera, {}) - -// camera.physical_viewport_rect() -// }); -// } -// } - /** * System in charge of updating a [`Camera`] when its window or projection changes. * diff --git a/src/systems/camera/ExtractCameras.ts b/src/systems/camera/ExtractCameras.ts index a6cf1a3..d90e039 100644 --- a/src/systems/camera/ExtractCameras.ts +++ b/src/systems/camera/ExtractCameras.ts @@ -3,9 +3,9 @@ import { Camera } from '../../components'; // TODO: support multi view export class ExtractCameras extends System { - cameras = this.query((q) => q.using(Camera).write); + cameras = this.query((q) => q.using(Camera).read); execute(): void { - this.cameras; + // this.cameras; } } diff --git a/src/systems/camera/OrbitControl.ts b/src/systems/camera/OrbitControl.ts index fcb6438..428cb79 100644 --- a/src/systems/camera/OrbitControl.ts +++ b/src/systems/camera/OrbitControl.ts @@ -42,6 +42,7 @@ export class OrbitControl extends System { for (const event of reader.read()) { if (event instanceof OrbitControlEvent.Orbit) { const delta = event.value; + console.log(dt, delta); look_angles.add_yaw(dt * -delta.x); look_angles.add_pitch(dt * delta.y); } else if (event instanceof OrbitControlEvent.TranslateTarget) { diff --git a/src/systems/light/AddClusters.ts b/src/systems/light/AddClusters.ts index 9adf6ea..a05ff29 100644 --- a/src/systems/light/AddClusters.ts +++ b/src/systems/light/AddClusters.ts @@ -1,9 +1,6 @@ import { System } from '@lastolivegames/becsy'; import { Camera, ClusterConfig, Clusters } from '../../components'; -/** - * Update Frustum - */ export class AddClusters extends System { configs = this.query( (q) => q.with(Camera).addedOrChanged.with(ClusterConfig).trackWrites, diff --git a/src/systems/light/BuildDirectionalLightCascades.ts b/src/systems/light/BuildDirectionalLightCascades.ts index f08a0fa..8f20573 100644 --- a/src/systems/light/BuildDirectionalLightCascades.ts +++ b/src/systems/light/BuildDirectionalLightCascades.ts @@ -1,4 +1,4 @@ -import { Entity, System } from '@lastolivegames/becsy'; +import { system, Entity, System } from '@lastolivegames/becsy'; import { AppConfig, Camera, @@ -12,10 +12,12 @@ import { Perspective, } from '../../components'; import { Mat4, Vec3, Vec4 } from '../../math'; +import { ClearDirectionalLightCascades } from './ClearDirectionalLightCascades'; /** * build_directional_light_cascades */ +@system((s) => s.after(ClearDirectionalLightCascades)) export class BuildDirectionalLightCascades extends System { private appConfig = this.singleton.read(AppConfig); @@ -30,7 +32,7 @@ export class BuildDirectionalLightCascades extends System { constructor() { super(); this.query((q) => q.using(Perspective, Orthographic).read); - this.query((q) => q.using(Cascades).write); + this.query((q) => q.using(Cascades).read); } execute(): void { @@ -95,7 +97,7 @@ export class BuildDirectionalLightCascades extends System { ); }); - const cascades = entity.write(Cascades); + const cascades = entity.read(Cascades); cascades.cascades.set(view_entity, view_cascades); }); }); diff --git a/src/systems/light/CheckLightMeshVisibility.ts b/src/systems/light/CheckLightMeshVisibility.ts index b82878f..3efb1ae 100644 --- a/src/systems/light/CheckLightMeshVisibility.ts +++ b/src/systems/light/CheckLightMeshVisibility.ts @@ -1,12 +1,147 @@ -import { Entity, System } from '@lastolivegames/becsy'; +import { Entity, System, system } from '@lastolivegames/becsy'; import { + Aabb, Cascades, CascadesFrusta, + CascadesVisibleEntities, DirectionalLight, Frustum, + GlobalTransform, + InheritedVisibility, + RenderLayers, + ViewVisibility, + VisibleEntities, } from '../../components'; /** * check_light_mesh_visibility */ -export class CheckLightMeshVisibility extends System {} +@system((s) => s.afterWritersOf(ViewVisibility)) +export class CheckLightMeshVisibility extends System { + directional_lights = this.query((q) => + q.current.with( + DirectionalLight, + CascadesFrusta, + CascadesVisibleEntities, + ViewVisibility, + ), + ); + + visible_entity_query = this.query((q) => + q.current + .with(InheritedVisibility, ViewVisibility) + .without(DirectionalLight), + ); + // (Without, Without), + + constructor() { + super(); + this.query((q) => q.using(RenderLayers, Aabb, GlobalTransform).read); + this.query((q) => q.using(ViewVisibility).write); + } + + execute(): void { + this.directional_lights.current.forEach((entity) => { + const directional_light = entity.read(DirectionalLight); + const frusta = entity.read(CascadesFrusta); + const visible_entities = entity.read(CascadesVisibleEntities); + const light_view_visibility = entity.read(ViewVisibility); + + // Re-use already allocated entries where possible. + const views_to_remove = []; + visible_entities.entities.forEach((cascade_view_entities, view) => { + const view_frusta = frusta.frusta.get(view); + + if (view_frusta) { + cascade_view_entities.length = view_frusta.length; + cascade_view_entities.forEach((x) => (x.entities = [])); + } else { + views_to_remove.push(view); + } + }); + frusta.frusta.forEach((frusta, view) => { + if (!visible_entities.entities.has(view)) { + visible_entities.entities.set( + view, + new Array(frusta.length).fill(new VisibleEntities()), + ); + } + }); + views_to_remove.forEach((v) => visible_entities.entities.delete(v)); + + // NOTE: If shadow mapping is disabled for the light then it must have no visible entities + if ( + !directional_light.shadows_enabled || + !light_view_visibility.visible + ) { + return; + } + + let view_mask: RenderLayers; + if (entity.has(RenderLayers)) { + view_mask = entity.read(RenderLayers); + } + + this.visible_entity_query.current.forEach((entity) => { + const inherited_visibility = entity.read(InheritedVisibility); + + if (!inherited_visibility.visible) { + return; + } + + let entity_mask: RenderLayers; + if (view_mask && entity.has(RenderLayers)) { + entity_mask = entity.read(RenderLayers); + if (!view_mask.intersects(entity_mask)) { + return; + } + } + + let maybe_aabb: Aabb; + if (entity.has(Aabb)) { + maybe_aabb = entity.read(Aabb); + } + let maybe_transform: GlobalTransform; + if (entity.has(GlobalTransform)) { + maybe_transform = GlobalTransform.copy(entity.read(GlobalTransform)); + } + const view_visibility = entity.write(ViewVisibility); + if (maybe_aabb && maybe_transform) { + frusta.frusta.forEach((view_frusta, view) => { + const view_visible_entities = visible_entities.entities.get(view); + + view_frusta.forEach((frustum, i) => { + const frustum_visible_entities = view_visible_entities[i]; + // Disable near-plane culling, as a shadow caster could lie before the near plane. + if ( + !frustum.intersects_obb( + maybe_aabb, + maybe_transform.affine(), + false, + true, + ) + ) { + return; + } + + view_visibility.visible = true; + frustum_visible_entities.entities.push(entity); + }); + }); + } else { + view_visibility.visible = true; + frusta.frusta.forEach((_, view) => { + const view_visible_entities = visible_entities.entities.get(view); + for (const frustum_visible_entities of view_visible_entities) { + frustum_visible_entities.entities.push(entity); + } + }); + } + }); + + // visible_entities.entities.forEach((cascade_view_entities) => { + // cascade_view_entities.forEach(shrink_entities); + // }); + }); + } +} diff --git a/src/systems/light/ClearDirectionalLightCascades.ts b/src/systems/light/ClearDirectionalLightCascades.ts index afd8266..91dcf9b 100644 --- a/src/systems/light/ClearDirectionalLightCascades.ts +++ b/src/systems/light/ClearDirectionalLightCascades.ts @@ -5,9 +5,12 @@ import { Cascades, DirectionalLight } from '../../components'; * clear_directional_light_cascades */ export class ClearDirectionalLightCascades extends System { - lights = this.query( - (q) => q.current.with(DirectionalLight).and.with(Cascades).write, - ); + lights = this.query((q) => q.current.with(DirectionalLight)); + + constructor() { + super(); + this.query((q) => q.using(Cascades).read); + } execute(): void { this.lights.current.forEach((entity) => { @@ -16,7 +19,7 @@ export class ClearDirectionalLightCascades extends System { return; } - const cascades = entity.write(Cascades); + const cascades = entity.read(Cascades); cascades.cascades.clear(); }); } diff --git a/src/systems/render/CheckVisibility.ts b/src/systems/render/CheckVisibility.ts index ae448ca..467ebd0 100644 --- a/src/systems/render/CheckVisibility.ts +++ b/src/systems/render/CheckVisibility.ts @@ -1,13 +1,16 @@ import { System } from '@lastolivegames/becsy'; import { + Aabb, Camera, Frustum, GlobalTransform, InheritedVisibility, + NoFrustumCulling, RenderLayers, ViewVisibility, VisibleEntities, } from '../../components'; +import { Sphere } from '../../components/primitive/Sphere'; /** * System updating the visibility of entities each frame. @@ -27,24 +30,72 @@ export class CheckVisibility extends System { constructor() { super(); - this.query((q) => q.using(VisibleEntities).write); + this.query((q) => q.using(VisibleEntities, ViewVisibility).write); + this.query((q) => q.using(RenderLayers, NoFrustumCulling, Aabb).read); } execute(): void { this.view_query.current.forEach((entity) => { const visible_entities = entity.write(VisibleEntities); const frustum = entity.read(Frustum); - const maybe_view_mask = entity.read(RenderLayers); const camera = entity.read(Camera); if (!camera.is_active) { return; } - const view_mask = maybe_view_mask; + let maybe_view_mask: RenderLayers; + if (entity.has(RenderLayers)) { + maybe_view_mask = entity.read(RenderLayers); + } + visible_entities.entities = []; - this.visible_aabb_query.current.forEach((entity) => {}); + this.visible_aabb_query.current.forEach((entity) => { + const inherited_visibility = entity.read(InheritedVisibility); + const view_visibility = entity.write(ViewVisibility); + const transform = GlobalTransform.copy(entity.read(GlobalTransform)); + + // Skip computing visibility for entities that are configured to be hidden. + // ViewVisibility has already been reset in `reset_view_visibility`. + if (!inherited_visibility.visible) { + return; + } + + if (entity.has(RenderLayers)) { + const view_mask = maybe_view_mask; + const maybe_entity_mask = entity.read(RenderLayers); + if (!view_mask.intersects(maybe_entity_mask)) { + return; + } + } + + const no_frustum_culling = entity.has(NoFrustumCulling); + // If we have an aabb, do frustum culling + if (!no_frustum_culling) { + if (entity.has(Aabb)) { + const model_aabb = entity.read(Aabb); + const model = transform.affine(); + + const model_sphere = new Sphere( + model.transform_point3(model_aabb.center), + transform.radius_vec3(model_aabb.half_extents), + ); + + // Do quick sphere-based frustum culling + if (!frustum.intersects_sphere(model_sphere, false)) { + return; + } + // Do aabb-based frustum culling + if (!frustum.intersects_obb(model_aabb, model, true, false)) { + return; + } + } + } + + view_visibility.visible = true; + visible_entities.entities.push(entity); + }); }); } }