diff --git a/examples/src/examples/graphics/clustered-lighting.example.mjs b/examples/src/examples/graphics/clustered-lighting.example.mjs index 4db6aac393e..2c03b6730d3 100644 --- a/examples/src/examples/graphics/clustered-lighting.example.mjs +++ b/examples/src/examples/graphics/clustered-lighting.example.mjs @@ -100,7 +100,6 @@ assetListLoader.load(() => { const cylinderMesh = pc.Mesh.fromGeometry(app.graphicsDevice, new pc.CylinderGeometry({ capSegments: 200 })); const cylinder = new pc.Entity(); cylinder.addComponent('render', { - material: material, meshInstances: [new pc.MeshInstance(cylinderMesh, material)], castShadows: true }); diff --git a/examples/src/examples/graphics/integer-textures.example.mjs b/examples/src/examples/graphics/integer-textures.example.mjs index bf7ea5e95ae..e7b876be3ed 100644 --- a/examples/src/examples/graphics/integer-textures.example.mjs +++ b/examples/src/examples/graphics/integer-textures.example.mjs @@ -81,13 +81,15 @@ const createPixelColorBuffer = (i) => { name: `PixelBuffer_${i}`, width: TEXTURE_WIDTH, height: TEXTURE_HEIGHT, + mipmaps: false, + addressU: pc.ADDRESS_CLAMP_TO_EDGE, + addressV: pc.ADDRESS_CLAMP_TO_EDGE, + // Note that we are using an unsigned integer format here. // This can be helpful for storing bitfields in each pixel. // In this example, we are storing 3 different properties // in a single Uint8 value. - format: pc.PIXELFORMAT_R8U, - addressU: pc.ADDRESS_CLAMP_TO_EDGE, - addressV: pc.ADDRESS_CLAMP_TO_EDGE + format: pc.PIXELFORMAT_R8U }); }; const createPixelRenderTarget = (i, colorBuffer) => { @@ -114,6 +116,7 @@ const outputTexture = new pc.Texture(device, { name: 'OutputTexture', width: TEXTURE_WIDTH, height: TEXTURE_HEIGHT, + mipmaps: false, format: pc.PIXELFORMAT_RGBA8, minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR, magFilter: pc.FILTER_LINEAR, diff --git a/examples/src/examples/graphics/outlines-colored.example.mjs b/examples/src/examples/graphics/outlines-colored.example.mjs index 7bfef730010..6bd3571e459 100644 --- a/examples/src/examples/graphics/outlines-colored.example.mjs +++ b/examples/src/examples/graphics/outlines-colored.example.mjs @@ -14,7 +14,6 @@ pc.WasmModule.setConfig('DracoDecoderModule', { const assets = { laboratory: new pc.Asset('statue', 'container', { url: `${rootPath}/static/assets/models/laboratory.glb` }), orbit: new pc.Asset('orbit', 'script', { url: `${rootPath}/static/scripts/camera/orbit-camera.js` }), - ssao: new pc.Asset('ssao', 'script', { url: `${rootPath}/static/scripts/posteffects/posteffect-ssao.js` }), helipad: new pc.Asset( 'helipad-env-atlas', 'texture', diff --git a/examples/src/examples/graphics/render-to-texture.example.mjs b/examples/src/examples/graphics/render-to-texture.example.mjs index c7eab5273d7..9cf6047e156 100644 --- a/examples/src/examples/graphics/render-to-texture.example.mjs +++ b/examples/src/examples/graphics/render-to-texture.example.mjs @@ -144,8 +144,6 @@ assetListLoader.load(() => { height: 256, format: pc.PIXELFORMAT_SRGBA8, mipmaps: true, - minFilter: pc.FILTER_LINEAR, - magFilter: pc.FILTER_LINEAR, addressU: pc.ADDRESS_CLAMP_TO_EDGE, addressV: pc.ADDRESS_CLAMP_TO_EDGE }); diff --git a/examples/src/examples/loaders/gsplat-many.example.mjs b/examples/src/examples/loaders/gsplat-many.example.mjs index c1d26e79997..273516db668 100644 --- a/examples/src/examples/loaders/gsplat-many.example.mjs +++ b/examples/src/examples/loaders/gsplat-many.example.mjs @@ -71,7 +71,6 @@ assetListLoader.load(() => { // instantiate guitar with a custom shader const guitar = assets.guitar.resource.instantiate({ - fragment: files['shader.frag'], vertex: files['shader.vert'] }); guitar.name = 'guitar'; diff --git a/examples/src/examples/loaders/gsplat-many.shader.frag b/examples/src/examples/loaders/gsplat-many.shader.frag deleted file mode 100644 index 42e16effb03..00000000000 --- a/examples/src/examples/loaders/gsplat-many.shader.frag +++ /dev/null @@ -1,34 +0,0 @@ -uniform float uTime; -varying float height; - -void animate(inout vec4 clr) { - float sineValue = abs(sin(uTime * 5.0 + height)); - - #ifdef CUTOUT - - // in cutout mode, remove pixels along the wave - if (sineValue < 0.5) { - clr.a = 0.0; - } - - #else - - // in non-cutout mode, add a golden tint to the wave - vec3 gold = vec3(1.0, 0.85, 0.0); - float blend = smoothstep(0.9, 1.0, sineValue); - clr.xyz = mix(clr.xyz, gold, blend); - - #endif -} - -varying mediump vec2 texCoord; -varying mediump vec4 color; - -void main(void) -{ - vec4 clr = evalSplat(texCoord, color); - - animate(clr); - - gl_FragColor = clr; -} \ No newline at end of file diff --git a/examples/src/examples/loaders/gsplat-many.shader.vert b/examples/src/examples/loaders/gsplat-many.shader.vert index 1eaa5209ced..4ff48299619 100644 --- a/examples/src/examples/loaders/gsplat-many.shader.vert +++ b/examples/src/examples/loaders/gsplat-many.shader.vert @@ -1,79 +1,82 @@ +#include "gsplatCommonVS" + +varying mediump vec2 gaussianUV; +varying mediump vec4 gaussianColor; + +#ifndef DITHER_NONE + varying float id; +#endif + +mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); + uniform float uTime; -varying float height; -void animate(inout vec3 center) { +vec3 animatePosition(vec3 center) { // modify center float heightIntensity = center.y * 0.2; center.x += sin(uTime * 5.0 + center.y) * 0.3 * heightIntensity; // output y-coordinate - height = center.y; + return center; } -uniform vec3 view_position; - -uniform sampler2D splatColor; - -varying mediump vec2 texCoord; -varying mediump vec4 color; +vec4 animateColor(float height, vec4 clr) { + float sineValue = abs(sin(uTime * 5.0 + height)); + + #ifdef CUTOUT + // in cutout mode, remove pixels along the wave + if (sineValue < 0.5) { + clr.a = 0.0; + } + #else + // in non-cutout mode, add a golden tint to the wave + vec3 gold = vec3(1.0, 0.85, 0.0); + float blend = smoothstep(0.9, 1.0, sineValue); + clr.xyz = mix(clr.xyz, gold, blend); + #endif -mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); + return clr; +} -void main(void) -{ - // calculate splat uv - if (!calcSplatUV()) { +void main(void) { + // read gaussian center + SplatSource source; + if (!initSource(source)) { gl_Position = discardVec; return; } - // get center - vec3 center = getCenter(); - - animate(center); + vec3 centerPos = animatePosition(readCenter(source)); - // handle transforms - mat4 model_view = matrix_view * matrix_model; - vec4 splat_cam = model_view * vec4(center, 1.0); - vec4 splat_proj = matrix_projection * splat_cam; + SplatCenter center; + initCenter(source, centerPos, center); - // cull behind camera - if (splat_proj.z < -splat_proj.w) { + // project center to screen space + SplatCorner corner; + if (!initCorner(source, center, corner)) { gl_Position = discardVec; return; } - // get covariance - vec3 covA, covB; - getCovariance(covA, covB); + // read color + vec4 clr = readColor(source); - vec4 v1v2 = calcV1V2(splat_cam.xyz, covA, covB, transpose(mat3(model_view))); - - // get color - color = texelFetch(splatColor, splatUV, 0); - - // calculate scale based on alpha - float scale = min(1.0, sqrt(-log(1.0 / 255.0 / color.a)) / 2.0); - - v1v2 *= scale; - - // early out tiny splats - if (dot(v1v2.xy, v1v2.xy) < 4.0 && dot(v1v2.zw, v1v2.zw) < 4.0) { - gl_Position = discardVec; - return; - } + // evaluate spherical harmonics + #if SH_BANDS > 0 + vec3 dir = normalize(center.view * mat3(center.modelView)); + clr.xyz += evalSH(state, dir); + #endif - gl_Position = splat_proj + vec4((vertex_position.x * v1v2.xy + vertex_position.y * v1v2.zw) / viewport * splat_proj.w, 0, 0); + clr = animateColor(centerPos.y, clr); - texCoord = vertex_position.xy * scale / 2.0; + clipCorner(corner, clr.w); - #ifdef USE_SH1 - vec4 worldCenter = matrix_model * vec4(center, 1.0); - vec3 viewDir = normalize((worldCenter.xyz / worldCenter.w - view_position) * mat3(matrix_model)); - color.xyz = max(color.xyz + evalSH(viewDir), 0.0); - #endif + // write output + gl_Position = center.proj + vec4(corner.offset, 0.0, 0.0); + gaussianUV = corner.uv; + gaussianColor = vec4(prepareOutputFromGamma(max(clr.xyz, 0.0)), clr.w); #ifndef DITHER_NONE - id = float(splatId); + id = float(state.id); #endif -} \ No newline at end of file +} diff --git a/package.json b/package.json index 0f8b0225135..45e504e8f93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "playcanvas", - "version": "2.3.0-dev", + "version": "2.4.0-dev", "author": "PlayCanvas ", "homepage": "https://playcanvas.com", "description": "PlayCanvas WebGL game engine", diff --git a/scripts/esm/camera-controls.mjs b/scripts/esm/camera-controls.mjs index a97a5650109..0cfc09249fd 100644 --- a/scripts/esm/camera-controls.mjs +++ b/scripts/esm/camera-controls.mjs @@ -848,20 +848,24 @@ class CameraControls extends Script { tmpV1.sub2(start, point); const elev = Math.atan2(tmpV1.y, Math.sqrt(tmpV1.x * tmpV1.x + tmpV1.z * tmpV1.z)) * math.RAD_TO_DEG; const azim = Math.atan2(tmpV1.x, tmpV1.z) * math.RAD_TO_DEG; - this._dir.set(-elev, -azim); + this._dir.set(this._clampPitch(-elev), azim); this._origin.copy(point); this._cameraTransform.setTranslate(0, 0, 0); - this._baseTransform.setTRS(this._origin, Quat.IDENTITY, Vec3.ONE); - this._zoomDist = tmpV1.length(); + const pos = this._camera.entity.getPosition(); + const rot = this._camera.entity.getRotation(); + this._baseTransform.setTRS(pos, rot, Vec3.ONE); + + this._zoomDist = this._clampZoom(tmpV1.length()); if (!smooth) { - this._angles.set(this._dir.x, this._dir.y, 0); - this._position.copy(point); - this._cameraDist = this._zoomDist; + this._smoothZoom(-1); + this._smoothTransform(-1); } + + this._updateTransform(); } /** @@ -947,6 +951,10 @@ class CameraControls extends Script { * @param {number} dt - The delta time. */ update(dt) { + if (this.app.xr?.active) { + return; + } + if (!this._camera) { return; } diff --git a/scripts/esm/xr-controllers.mjs b/scripts/esm/xr-controllers.mjs new file mode 100644 index 00000000000..19509026f52 --- /dev/null +++ b/scripts/esm/xr-controllers.mjs @@ -0,0 +1,101 @@ +/* eslint-disable-next-line import/no-unresolved */ +import { Script } from 'playcanvas'; + +export default class XrControllers extends Script { + /** + * The base URL for fetching the WebXR input profiles. + * + * @attribute + * @type {string} + */ + basePath = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets/dist/profiles'; + + controllers = new Map(); + + initialize() { + this.app.xr.input.on('add', async (inputSource) => { + if (!inputSource.profiles?.length) { + console.warn('No profiles available for input source'); + return; + } + + // Process all profiles concurrently + const profilePromises = inputSource.profiles.map(async (profileId) => { + const profileUrl = `${this.basePath}/${profileId}/profile.json`; + + try { + const response = await fetch(profileUrl); + if (!response.ok) { + return null; + } + + const profile = await response.json(); + const layoutPath = profile.layouts[inputSource.handedness]?.assetPath || ''; + const assetPath = `${this.basePath}/${profile.profileId}/${inputSource.handedness}${layoutPath.replace(/^\/?(left|right)/, '')}`; + + // Load the model + const asset = await new Promise((resolve, reject) => { + this.app.assets.loadFromUrl(assetPath, 'container', (err, asset) => { + if (err) reject(err); + else resolve(asset); + }); + }); + + return { profileId, asset }; + } catch (error) { + console.warn(`Failed to process profile ${profileId}`); + return null; + } + }); + + // Wait for all profile attempts to complete + const results = await Promise.all(profilePromises); + const successfulResult = results.find(result => result !== null); + + if (successfulResult) { + const { asset } = successfulResult; + const container = asset.resource; + const entity = container.instantiateRenderEntity(); + this.app.root.addChild(entity); + + const jointMap = new Map(); + if (inputSource.hand) { + for (const joint of inputSource.hand.joints) { + const jointEntity = entity.findByName(joint.id); + if (jointEntity) { + jointMap.set(joint, jointEntity); + } + } + } + + this.controllers.set(inputSource, { entity, jointMap }); + } else { + console.warn('No compatible profiles found'); + } + }); + + this.app.xr.input.on('remove', (inputSource) => { + const controller = this.controllers.get(inputSource); + if (controller) { + controller.entity.destroy(); + this.controllers.delete(inputSource); + } + }); + } + + update(dt) { + if (this.app.xr.active) { + for (const [inputSource, { entity, jointMap }] of this.controllers) { + if (inputSource.hand) { + for (const [joint, jointEntity] of jointMap) { + jointEntity.setPosition(joint.getPosition()); + jointEntity.setRotation(joint.getRotation()); + } + } else { + entity.setPosition(inputSource.getPosition()); + entity.setRotation(inputSource.getRotation()); + } + } + } + } +} diff --git a/scripts/esm/xr-navigation.mjs b/scripts/esm/xr-navigation.mjs new file mode 100644 index 00000000000..2fbec26f3ce --- /dev/null +++ b/scripts/esm/xr-navigation.mjs @@ -0,0 +1,133 @@ +/* eslint-disable-next-line import/no-unresolved */ +import { Color, Script, Vec3 } from 'playcanvas'; + +/** @import { XrInputSource } from 'playcanvas' */ + +/** + * Handles VR teleportation navigation by allowing users to point and teleport using either + * hands or tracked controllers. Shows a visual ray and target indicator when the user holds + * the select button (trigger) or makes a pinch gesture with hand tracking, and teleports to + * the target location when released. + * + * This script should be attached to a parent entity of the camera entity used for the XR + * session. Use it in conjunction with the `XrControllers` script to handle the rendering of + * the controllers. + */ +export class XrNavigation extends Script { + /** @type {Set} */ + inputSources = new Set(); + + /** @type {Map} */ + activePointers = new Map(); + + validColor = new Color(0, 1, 0); // Green for valid teleport + + invalidColor = new Color(1, 0, 0); // Red for invalid teleport + + /** @type {Map} */ + inputHandlers = new Map(); + + initialize() { + this.app.xr.input.on('add', (inputSource) => { + const handleSelectStart = () => { + this.activePointers.set(inputSource, true); + }; + + const handleSelectEnd = () => { + this.activePointers.set(inputSource, false); + this.tryTeleport(inputSource); + }; + + // Attach the handlers + inputSource.on('selectstart', handleSelectStart); + inputSource.on('selectend', handleSelectEnd); + + // Store the handlers in the map + this.inputHandlers.set(inputSource, { handleSelectStart, handleSelectEnd }); + this.inputSources.add(inputSource); + }); + + this.app.xr.input.on('remove', (inputSource) => { + const handlers = this.inputHandlers.get(inputSource); + if (handlers) { + inputSource.off('selectstart', handlers.handleSelectStart); + inputSource.off('selectend', handlers.handleSelectEnd); + this.inputHandlers.delete(inputSource); + } + this.activePointers.delete(inputSource); + this.inputSources.delete(inputSource); + }); + } + + findPlaneIntersection(origin, direction) { + // Find intersection with y=0 plane + if (Math.abs(direction.y) < 0.00001) return null; // Ray is parallel to plane + + const t = -origin.y / direction.y; + if (t < 0) return null; // Intersection is behind the ray + + return new Vec3( + origin.x + direction.x * t, + 0, + origin.z + direction.z * t + ); + } + + tryTeleport(inputSource) { + const origin = inputSource.getOrigin(); + const direction = inputSource.getDirection(); + + const hitPoint = this.findPlaneIntersection(origin, direction); + if (hitPoint) { + const cameraY = this.entity.getPosition().y; + hitPoint.y = cameraY; + this.entity.setPosition(hitPoint); + } + } + + update() { + for (const inputSource of this.inputSources) { + // Only show ray when trigger is pressed + if (!this.activePointers.get(inputSource)) continue; + + const start = inputSource.getOrigin(); + const direction = inputSource.getDirection(); + + const hitPoint = this.findPlaneIntersection(start, direction); + + if (hitPoint) { + // Draw line to intersection point + this.app.drawLine(start, hitPoint, this.validColor); + this.drawTeleportIndicator(hitPoint); + } else { + // Draw full length ray if no intersection + const end = start.clone().add( + direction.clone().mulScalar(100) + ); + this.app.drawLine(start, end, this.invalidColor); + } + } + } + + drawTeleportIndicator(point) { + // Draw a circle at the teleport point + const segments = 32; + const radius = 0.2; + + for (let i = 0; i < segments; i++) { + const angle1 = (i / segments) * Math.PI * 2; + const angle2 = ((i + 1) / segments) * Math.PI * 2; + + const x1 = point.x + Math.cos(angle1) * radius; + const z1 = point.z + Math.sin(angle1) * radius; + const x2 = point.x + Math.cos(angle2) * radius; + const z2 = point.z + Math.sin(angle2) * radius; + + this.app.drawLine( + new Vec3(x1, 0.01, z1), // Slightly above ground to avoid z-fighting + new Vec3(x2, 0.01, z2), + this.validColor + ); + } + } +} diff --git a/scripts/utils/camera-frame.mjs b/scripts/utils/camera-frame.mjs index 6431258b2bc..1b8265211ed 100644 --- a/scripts/utils/camera-frame.mjs +++ b/scripts/utils/camera-frame.mjs @@ -167,7 +167,7 @@ class Bloom { * @precision 0 * @step 0 */ - blurLevel = 1; + blurLevel = 16; } /** @interface */ diff --git a/scripts/utils/cubemap-renderer.js b/scripts/utils/cubemap-renderer.js index 782c96ec9df..8db3f394d9b 100644 --- a/scripts/utils/cubemap-renderer.js +++ b/scripts/utils/cubemap-renderer.js @@ -67,6 +67,8 @@ CubemapRenderer.prototype.initialize = function () { ]; // set up rendering for all 6 faces + let firstCamera = null; + let lastCamera = null; for (var i = 0; i < 6; i++) { // render target, connected to cubemap texture face @@ -109,19 +111,29 @@ CubemapRenderer.prototype.initialize = function () { // set up its rotation e.setRotation(cameraRotations[i]); - // Before the first camera renders, trigger onCubemapPreRender event on the entity. - if (i === 0) { - e.camera.onPreRender = () => { - this.entity.fire('onCubemapPreRender'); - }; + // keep the first and last camera + if (i === 0) firstCamera = e.camera; + if (i === 5) lastCamera = e.camera; + } + + // Before the first camera renders, trigger onCubemapPreRender event on the entity. + this.evtPreRender = this.app.scene.on('prerender', (cameraComponent) => { + if (cameraComponent === firstCamera) { + this.entity.fire('onCubemapPreRender'); } + }); - // When last camera is finished rendering, trigger onCubemapPostRender event on the entity. - // This can be listened to by the user, and the resulting cubemap can be further processed (e.g prefiltered) - if (i === 5) { - e.camera.onPostRender = () => { - this.entity.fire('onCubemapPostRender'); - }; + // When last camera is finished rendering, trigger onCubemapPostRender event on the entity. + // This can be listened to by the user, and the resulting cubemap can be further processed (e.g pre-filtering) + this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => { + if (cameraComponent === lastCamera) { + this.entity.fire('onCubemapPostRender'); } - } + }); + + // when the script is destroyed, remove event listeners + this.on('destroy', () => { + this.evtPreRender.off(); + this.evtPostRender.off(); + }); }; diff --git a/scripts/utils/mac-gpu-profiling.js b/scripts/utils/mac-gpu-profiling.js new file mode 100644 index 00000000000..90e95d67377 --- /dev/null +++ b/scripts/utils/mac-gpu-profiling.js @@ -0,0 +1,96 @@ +/** + * This script allows GPU Profiling on Mac using Xcode's GPU Frame Capture. Please read the instructions + * in the manual: https://developer.playcanvas.com/user-manual/optimization/gpu-profiling/ + */ +var MacGPUProfiling = pc.createScript('MacGPUProfiling'); + +// Called once after all resources are loaded and initialized +MacGPUProfiling.prototype.initialize = function () { + this.isInitialized = false; + this.device = null; + this.context = null; + + // this is not needed for WebGPU + if (this.app.graphicsDevice.isWebGPU) return; + + // only needed on Mac + if (pc.platform.name !== 'osx') return; + + // Create a new canvas for WebGPU with a smaller size + this.webgpuCanvas = document.createElement('canvas'); + this.webgpuCanvas.width = 20; + this.webgpuCanvas.height = 20; + this.webgpuCanvas.style.position = 'absolute'; + this.webgpuCanvas.style.top = '20px'; // Adjust position if needed + this.webgpuCanvas.style.left = '20px'; // Adjust position if needed + document.body.appendChild(this.webgpuCanvas); + + // Start the asynchronous WebGPU initialization + this.initWebGPU(); +}; + +// Async function for WebGPU initialization +MacGPUProfiling.prototype.initWebGPU = async function () { + // Check for WebGPU support + if (!navigator.gpu) { + console.error('WebGPU is not supported on this browser.'); + return; + } + + // Get WebGPU adapter and device + const adapter = await navigator.gpu.requestAdapter(); + this.device = await adapter.requestDevice(); + + console.log('Created WebGPU device used for profiling'); + + // Create a WebGPU context for the new canvas + this.context = this.webgpuCanvas.getContext('webgpu'); + + // Configure the WebGPU context + const swapChainFormat = 'bgra8unorm'; + this.context.configure({ + device: this.device, + format: swapChainFormat + }); + + // Mark initialization as complete + this.isInitialized = true; + + // Hook into the 'frameend' event + this.app.on('frameend', this.onFrameEnd, this); +}; + +// Called when the 'frameend' event is triggered +MacGPUProfiling.prototype.onFrameEnd = function () { + // If WebGPU is not initialized yet, do nothing + if (!this.isInitialized) return; + + // Clear the WebGPU surface to red after WebGL rendering + this.clearToRed(); +}; + +// Function to clear the WebGPU surface to red +MacGPUProfiling.prototype.clearToRed = function () { + // Get the current texture to render to + const textureView = this.context.getCurrentTexture().createView(); + + // Create a command encoder + const commandEncoder = this.device.createCommandEncoder(); + + // Create a render pass descriptor with a red background + const renderPassDescriptor = { + colorAttachments: [{ + view: textureView, + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Red background + loadOp: 'clear', + storeOp: 'store' + }] + }; + + // render pass + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.end(); + + // Submit the commands to the GPU + this.device.queue.submit([commandEncoder.finish()]); +}; diff --git a/scripts/utils/planar-renderer.js b/scripts/utils/planar-renderer.js index c979c923384..7c39df755c1 100644 --- a/scripts/utils/planar-renderer.js +++ b/scripts/utils/planar-renderer.js @@ -68,9 +68,16 @@ PlanarRenderer.prototype.initialize = function () { // When the camera is finished rendering, trigger onPlanarPostRender event on the entity. // This can be listened to by the user, and the resulting texture can be further processed (e.g prefiltered) - planarCamera.onPostRender = () => { - this.entity.fire('onPlanarPostRender'); - }; + this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => { + if (planarCamera === cameraComponent) { + this.entity.fire('onPlanarPostRender'); + } + }); + + // when the script is destroyed, remove event listeners + this.on('destroy', () => { + this.evtPostRender.off(); + }); }; PlanarRenderer.prototype.updateRenderTarget = function () { diff --git a/src/core/event-handle.js b/src/core/event-handle.js index 58c2c15abbf..9894e0f7e48 100644 --- a/src/core/event-handle.js +++ b/src/core/event-handle.js @@ -39,7 +39,7 @@ class EventHandle { /** * @type {string} - * @private + * @ignore */ name; @@ -88,7 +88,7 @@ class EventHandle { */ off() { if (this._removed) return; - this.handler.off(this.name, this.callback, this.scope); + this.handler.offByHandle(this); } on(name, callback, scope = this) { diff --git a/src/core/event-handler.js b/src/core/event-handler.js index be1f6d10c18..a20e84a720b 100644 --- a/src/core/event-handler.js +++ b/src/core/event-handler.js @@ -221,6 +221,40 @@ class EventHandler { return this; } + /** + * Detach an event handler from an event using EventHandle instance. More optimal remove + * as it does not have to scan callbacks array. + * + * @param {EventHandle} handle - Handle of event. + * @ignore + */ + offByHandle(handle) { + const name = handle.name; + handle.removed = true; + + // if we are removing a callback from the list that is executing right now + // ensure we preserve initial list before modifications + if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) { + this._callbackActive.set(name, this._callbackActive.get(name).slice()); + } + + const callbacks = this._callbacks.get(name); + if (!callbacks) { + return this; + } + + const ind = callbacks.indexOf(handle); + if (ind !== -1) { + callbacks.splice(ind, 1); + + if (callbacks.length === 0) { + this._callbacks.delete(name); + } + } + + return this; + } + /** * Fire an event, all additional arguments are passed on to the event listener. * diff --git a/src/core/hash.js b/src/core/hash.js index 8523a8d28f9..3e67645b4eb 100644 --- a/src/core/hash.js +++ b/src/core/hash.js @@ -5,6 +5,9 @@ * @returns {number} Hash value. */ function hashCode(str) { + if (str === null || str === undefined) { + return 0; + } let hash = 0; for (let i = 0, len = str.length; i < len; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js index 1084144cf1f..e78a28c88f3 100644 --- a/src/deprecated/deprecated.js +++ b/src/deprecated/deprecated.js @@ -59,6 +59,7 @@ import { RigidBodyComponent } from '../framework/components/rigid-body/component import { RigidBodyComponentSystem } from '../framework/components/rigid-body/system.js'; import { LitShader } from '../scene/shader-lib/programs/lit-shader.js'; import { Geometry } from '../scene/geometry/geometry.js'; +import { CameraComponent } from '../framework/components/camera/component.js'; // MATH @@ -564,30 +565,37 @@ Object.defineProperty(Scene.prototype, 'models', { } }); -// A helper function to add deprecated set and get property on a Layer -function _removedLayerProperty(name) { - Object.defineProperty(Layer.prototype, name, { +// A helper function to add deprecated set and get property on a class +function _removedClassProperty(targetClass, name, comment = '') { + Object.defineProperty(targetClass.prototype, name, { set: function (value) { - Debug.errorOnce(`pc.Layer#${name} has been removed.`); + Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`); }, get: function () { - Debug.errorOnce(`pc.Layer#${name} has been removed.`); + Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`); return undefined; } }); } -_removedLayerProperty('renderTarget'); -_removedLayerProperty('onPreCull'); -_removedLayerProperty('onPreRender'); -_removedLayerProperty('onPreRenderOpaque'); -_removedLayerProperty('onPreRenderTransparent'); -_removedLayerProperty('onPostCull'); -_removedLayerProperty('onPostRender'); -_removedLayerProperty('onPostRenderOpaque'); -_removedLayerProperty('onPostRenderTransparent'); -_removedLayerProperty('onDrawCall'); -_removedLayerProperty('layerReference'); +_removedClassProperty(Layer, 'renderTarget'); +_removedClassProperty(Layer, 'onPreCull'); +_removedClassProperty(Layer, 'onPreRender'); +_removedClassProperty(Layer, 'onPreRenderOpaque'); +_removedClassProperty(Layer, 'onPreRenderTransparent'); +_removedClassProperty(Layer, 'onPostCull'); +_removedClassProperty(Layer, 'onPostRender'); +_removedClassProperty(Layer, 'onPostRenderOpaque'); +_removedClassProperty(Layer, 'onPostRenderTransparent'); +_removedClassProperty(Layer, 'onDrawCall'); +_removedClassProperty(Layer, 'layerReference'); + +_removedClassProperty(CameraComponent, 'onPreCull', 'Use Scene#EVENT_PRECULL event instead.'); +_removedClassProperty(CameraComponent, 'onPostCull', 'Use Scene#EVENT_POSTCULL event instead.'); +_removedClassProperty(CameraComponent, 'onPreRender', 'Use Scene#EVENT_PRERENDER event instead.'); +_removedClassProperty(CameraComponent, 'onPostRender', 'Use Scene#EVENT_POSTRENDER event instead.'); +_removedClassProperty(CameraComponent, 'onPreRenderLayer', 'Use Scene#EVENT_PRERENDER_LAYER event instead.'); +_removedClassProperty(CameraComponent, 'onPostRenderLayer', 'Use Scene#EVENT_POSTRENDER_LAYER event instead.'); ForwardRenderer.prototype.renderComposition = function (comp) { Debug.deprecated('pc.ForwardRenderer#renderComposition is deprecated. Use pc.AppBase.renderComposition instead.'); diff --git a/src/extras/exporters/gltf-exporter.js b/src/extras/exporters/gltf-exporter.js index a1c99e5f550..4f75620002a 100644 --- a/src/extras/exporters/gltf-exporter.js +++ b/src/extras/exporters/gltf-exporter.js @@ -242,15 +242,21 @@ class GltfExporter extends CoreExporter { // FIXME: don't create the function every time const addBufferView = (target, byteLength, byteOffset, byteStride) => { - const bufferView = { - target: target, buffer: 0, byteLength: byteLength, - byteOffset: byteOffset, - byteStride: byteStride + byteOffset: byteOffset }; + // Only add target if it's a vertex or index buffer + if (target === ARRAY_BUFFER || target === ELEMENT_ARRAY_BUFFER) { + bufferView.target = target; + } + + if (byteStride !== undefined) { + bufferView.byteStride = byteStride; + } + return json.bufferViews.push(bufferView) - 1; }; @@ -260,16 +266,12 @@ class GltfExporter extends CoreExporter { const format = buffer.getFormat(); if (format.interleaved) { - const bufferViewIndex = addBufferView(ARRAY_BUFFER, arrayBuffer.byteLength, offset, format.size); resources.bufferViewMap.set(buffer, [bufferViewIndex]); - } else { - // generate buffer view per element const bufferViewIndices = []; for (const element of format.elements) { - const bufferViewIndex = addBufferView( ARRAY_BUFFER, element.size * format.vertexCount, @@ -277,25 +279,18 @@ class GltfExporter extends CoreExporter { element.size ); bufferViewIndices.push(bufferViewIndex); - } - resources.bufferViewMap.set(buffer, bufferViewIndices); } - - } else if (buffer instanceof IndexBuffer) { // index buffer + } else if (buffer instanceof IndexBuffer) { arrayBuffer = buffer.lock(); - - const bufferViewIndex = addBufferView(ARRAY_BUFFER, arrayBuffer.byteLength, offset); + const bufferViewIndex = addBufferView(ELEMENT_ARRAY_BUFFER, arrayBuffer.byteLength, offset); resources.bufferViewMap.set(buffer, [bufferViewIndex]); - } else { - // buffer is an array buffer + // buffer is an array buffer (for images) arrayBuffer = buffer; - - const bufferViewIndex = addBufferView(ELEMENT_ARRAY_BUFFER, arrayBuffer.byteLength, offset); + const bufferViewIndex = addBufferView(undefined, arrayBuffer.byteLength, offset); resources.bufferViewMap.set(buffer, [bufferViewIndex]); - } // increment buffer by the size of the array buffer to allocate buffer with enough space @@ -492,21 +487,20 @@ class GltfExporter extends CoreExporter { } } - writeMeshes(resources, json) { + writeMeshes(resources, json, options) { if (resources.entityMeshInstances.length > 0) { json.accessors = []; json.meshes = []; resources.entityMeshInstances.forEach((entityMeshInstances) => { - const mesh = { primitives: [] }; - // all mesh instances of a single node are stores as a single gltf mesh with multiple primitives + // all mesh instances of a single node are stored as a single gltf mesh with multiple primitives const meshInstances = entityMeshInstances.meshInstances; meshInstances.forEach((meshInstance) => { - const primitive = GltfExporter.createPrimitive(resources, json, meshInstance.mesh); + const primitive = GltfExporter.createPrimitive(resources, json, meshInstance.mesh, options); primitive.material = resources.materials.indexOf(meshInstance.material); @@ -518,7 +512,7 @@ class GltfExporter extends CoreExporter { } } - static createPrimitive(resources, json, mesh) { + static createPrimitive(resources, json, mesh, options = {}) { const primitive = { attributes: {} }; @@ -529,6 +523,43 @@ class GltfExporter extends CoreExporter { const { interleaved, elements } = format; const numVertices = vertexBuffer.getNumVertices(); elements.forEach((element, elementIndex) => { + const semantic = getSemantic(element.name); + + // Skip unused attributes if stripping is enabled + if (options.stripUnusedAttributes) { + let isUsed = true; + + // Check texture coordinates + if (semantic.startsWith('TEXCOORD_')) { + const texCoordIndex = parseInt(semantic.split('_')[1], 10); + isUsed = resources.materials.some((material) => { + return textureSemantics.some((texSemantic) => { + const texture = material[texSemantic]; + // Most materials use UV0 by default, so keep TEXCOORD_0 unless explicitly using a different UV set + return texture && (texCoordIndex === 0 || material[`${texSemantic}Tiling`]?.uv === texCoordIndex); + }); + }); + } + + // Check vertex colors + if (semantic === 'COLOR_0') { + isUsed = resources.materials.some(material => material.vertexColors); + } + + // Check tangents + if (semantic === 'TANGENT') { + isUsed = resources.materials.some(material => material.normalMap); + } + + // Check skinning attributes + if (semantic === 'JOINTS_0' || semantic === 'WEIGHTS_0') { + isUsed = resources.entityMeshInstances.some(emi => emi.meshInstances.some(mi => mi.mesh.skin)); + } + + if (!isUsed) { + return; // Skip this attribute + } + } let bufferView = resources.bufferViewMap.get(vertexBuffer); if (!bufferView) { @@ -548,11 +579,10 @@ class GltfExporter extends CoreExporter { }; const idx = json.accessors.push(accessor) - 1; - primitive.attributes[getSemantic(element.name)] = idx; + primitive.attributes[semantic] = idx; // Position accessor also requires min and max properties if (element.name === SEMANTIC_POSITION) { - // compute min and max from positions, as the BoundingBox stores center and extents, // and we get precision warnings from gltf validator const positions = []; @@ -728,7 +758,7 @@ class GltfExporter extends CoreExporter { this.writeBufferViews(resources, json); this.writeCameras(resources, json); - this.writeMeshes(resources, json); + this.writeMeshes(resources, json, options); this.writeMaterials(resources, json); this.writeNodes(resources, json, options); await this.writeTextures(resources, textureCanvases, json, options); @@ -747,8 +777,15 @@ class GltfExporter extends CoreExporter { * * @param {Entity} entity - The root of the entity hierarchy to convert. * @param {object} options - Object for passing optional arguments. - * @param {number} [options.maxTextureSize] - Maximum texture size. Texture is resized if over - * the size. + * @param {number} [options.maxTextureSize] - Maximum texture size. Texture is resized if over the size. + * @param {boolean} [options.stripUnusedAttributes] - If true, removes unused vertex attributes: + * + * - Texture coordinates not referenced by materials + * - Vertex colors if not used by materials + * - Tangents if no normal maps are used + * - Skinning data if no skinned meshes exist + * + * Defaults to false. * @returns {Promise} - The GLB file content. */ build(entity, options = {}) { diff --git a/src/extras/render-passes/render-pass-bloom.js b/src/extras/render-passes/render-pass-bloom.js index ae91d86682b..2d382110b69 100644 --- a/src/extras/render-passes/render-pass-bloom.js +++ b/src/extras/render-passes/render-pass-bloom.js @@ -9,6 +9,10 @@ import { RenderPassDownsample } from './render-pass-downsample.js'; import { RenderPassUpsample } from './render-pass-upsample.js'; import { math } from '../../core/math/math.js'; +/** + * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' + */ + // based on https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom /** * Render pass implementation of HDR bloom effect. @@ -27,6 +31,12 @@ class RenderPassBloom extends RenderPass { renderTargets = []; + /** + * @param {GraphicsDevice} device - The graphics device. + * @param {Texture} sourceTexture - The source texture, usually at half the resolution of the + * render target getting blurred. + * @param {number} format - The texture format. + */ constructor(device, sourceTexture, format) { super(device); this._sourceTexture = sourceTexture; @@ -95,8 +105,7 @@ class RenderPassBloom extends RenderPass { let passSourceTexture = this._sourceTexture; for (let i = 0; i < numPasses; i++) { - const fast = i === 0; // fast box downscale for the first pass - const pass = new RenderPassDownsample(device, passSourceTexture, fast); + const pass = new RenderPassDownsample(device, passSourceTexture); const rt = this.renderTargets[i]; pass.init(rt, { resizeSource: passSourceTexture, @@ -130,24 +139,6 @@ class RenderPassBloom extends RenderPass { this.destroyRenderTargets(1); } - set sourceTexture(value) { - this._sourceTexture = value; - - if (this.beforePasses.length > 0) { - const firstPass = this.beforePasses[0]; - - // change resize source - firstPass.options.resizeSource = value; - - // change downsample source - firstPass.sourceTexture = value; - } - } - - get sourceTexture() { - return this._sourceTexture; - } - frameUpdate() { super.frameUpdate(); diff --git a/src/extras/render-passes/render-pass-camera-frame.js b/src/extras/render-passes/render-pass-camera-frame.js index 7e539c4c860..8db06c6b60d 100644 --- a/src/extras/render-passes/render-pass-camera-frame.js +++ b/src/extras/render-passes/render-pass-camera-frame.js @@ -13,6 +13,8 @@ import { RenderPassPrepass } from './render-pass-prepass.js'; import { RenderPassSsao } from './render-pass-ssao.js'; import { SSAOTYPE_COMBINE, SSAOTYPE_LIGHTING, SSAOTYPE_NONE } from './constants.js'; import { Debug } from '../../core/debug.js'; +import { RenderPassDownsample } from './render-pass-downsample.js'; +import { Color } from '../../core/math/color.js'; class CameraFrameOptions { formats; @@ -71,6 +73,8 @@ class RenderPassCameraFrame extends RenderPass { taaPass; + scenePassHalf; + _renderTargetScale = 1; /** @@ -96,6 +100,7 @@ class RenderPassCameraFrame extends RenderPass { reset() { this.sceneTexture = null; + this.sceneTextureHalf = null; if (this.rt) { this.rt.destroyTextureBuffers(); @@ -103,6 +108,12 @@ class RenderPassCameraFrame extends RenderPass { this.rt = null; } + if (this.rtHalf) { + this.rtHalf.destroyTextureBuffers(); + this.rtHalf.destroy(); + this.rtHalf = null; + } + // destroy all passes we created this.beforePasses.forEach(pass => pass.destroy()); this.beforePasses.length = 0; @@ -116,6 +127,7 @@ class RenderPassCameraFrame extends RenderPass { this.ssaoPass = null; this.taaPass = null; this.afterPass = null; + this.scenePassHalf = null; } sanitizeOptions(options) { @@ -180,6 +192,29 @@ class RenderPassCameraFrame extends RenderPass { } } + createRenderTarget(name, depth, stencil, samples, flipY) { + + const texture = new Texture(this.device, { + name: name, + width: 4, + height: 4, + format: this.hdrFormat, + mipmaps: false, + minFilter: FILTER_LINEAR, + magFilter: FILTER_LINEAR, + addressU: ADDRESS_CLAMP_TO_EDGE, + addressV: ADDRESS_CLAMP_TO_EDGE + }); + + return new RenderTarget({ + colorBuffer: texture, + depth: depth, + stencil: stencil, + samples: samples, + flipY: flipY + }); + } + setupRenderPasses(options) { const { device } = this; @@ -188,6 +223,12 @@ class RenderPassCameraFrame extends RenderPass { this.hdrFormat = device.getRenderableHdrFormat(options.formats, true, options.samples) || PIXELFORMAT_RGBA8; + // HDR bloom is not supported on RGBA8 format + this._bloomEnabled = options.bloomEnabled && this.hdrFormat !== PIXELFORMAT_RGBA8; + + // bloom needs half resolution scene texture + this._sceneHalfEnabled = this._bloomEnabled; + // camera renders in HDR mode (linear output, no tonemapping) cameraComponent.gammaCorrection = GAMMA_NONE; cameraComponent.toneMapping = TONEMAP_NONE; @@ -196,25 +237,15 @@ class RenderPassCameraFrame extends RenderPass { cameraComponent.shaderParams.ssaoEnabled = options.ssaoType === SSAOTYPE_LIGHTING; // create a render target to render the scene into - this.sceneTexture = new Texture(device, { - name: 'SceneColor', - width: 4, - height: 4, - format: this.hdrFormat, - mipmaps: false, - minFilter: FILTER_LINEAR, - magFilter: FILTER_LINEAR, - addressU: ADDRESS_CLAMP_TO_EDGE, - addressV: ADDRESS_CLAMP_TO_EDGE - }); - - this.rt = new RenderTarget({ - colorBuffer: this.sceneTexture, - depth: true, - stencil: options.stencil, - samples: options.samples, - flipY: !!targetRenderTarget?.flipY // flipY is inherited from the target renderTarget - }); + const flipY = !!targetRenderTarget?.flipY; // flipY is inherited from the target renderTarget + this.rt = this.createRenderTarget('SceneColor', true, options.stencil, options.samples, flipY); + this.sceneTexture = this.rt.colorBuffer; + + // when half size scene color buffer is used + if (this._sceneHalfEnabled) { + this.rtHalf = this.createRenderTarget('SceneColorHalf', false, false, 1, flipY); + this.sceneTextureHalf = this.rtHalf.colorBuffer; + } this.sceneOptions = { resizeSource: targetRenderTarget, @@ -231,7 +262,7 @@ class RenderPassCameraFrame extends RenderPass { collectPasses() { // use these prepared render passes in the order they should be executed - return [this.prePass, this.ssaoPass, this.scenePass, this.colorGrabPass, this.scenePassTransparent, this.taaPass, this.bloomPass, this.composePass, this.afterPass]; + return [this.prePass, this.ssaoPass, this.scenePass, this.colorGrabPass, this.scenePassTransparent, this.taaPass, this.scenePassHalf, this.bloomPass, this.composePass, this.afterPass]; } createPasses(options) { @@ -248,8 +279,11 @@ class RenderPassCameraFrame extends RenderPass { // TAA const sceneTextureWithTaa = this.setupTaaPass(options); + // downscale to half resolution + this.setupSceneHalfPass(options, sceneTextureWithTaa); + // bloom - this.setupBloomPass(options, sceneTextureWithTaa); + this.setupBloomPass(options, this.sceneTextureHalf); // compose this.setupComposePass(options); @@ -328,9 +362,23 @@ class RenderPassCameraFrame extends RenderPass { } } + setupSceneHalfPass(options, sourceTexture) { + + if (this._sceneHalfEnabled) { + this.scenePassHalf = new RenderPassDownsample(this.device, this.sceneTexture, true); + this.scenePassHalf.name = 'RenderPassSceneHalf'; + this.scenePassHalf.init(this.rtHalf, { + resizeSource: sourceTexture, + scaleX: 0.5, + scaleY: 0.5 + }); + this.scenePassHalf.setClearColor(Color.BLACK); + } + } + setupBloomPass(options, inputTexture) { - // HDR bloom is not supported on RGBA8 format - if (options.bloomEnabled && this.hdrFormat !== PIXELFORMAT_RGBA8) { + + if (this._bloomEnabled) { // create a bloom pass, which generates bloom texture based on the provided texture this.bloomPass = new RenderPassBloom(this.device, inputTexture, this.hdrFormat); } @@ -387,9 +435,7 @@ class RenderPassCameraFrame extends RenderPass { // TAA history buffer is double buffered, assign the current one to the follow up passes. this.composePass.sceneTexture = sceneTexture; - if (this.options.bloomEnabled && this.bloomPass) { - this.bloomPass.sourceTexture = sceneTexture; - } + this.scenePassHalf?.setSourceTexture(sceneTexture); } } diff --git a/src/extras/render-passes/render-pass-compose.js b/src/extras/render-passes/render-pass-compose.js index b2619a7b599..7419f1b67a5 100644 --- a/src/extras/render-passes/render-pass-compose.js +++ b/src/extras/render-passes/render-pass-compose.js @@ -2,9 +2,7 @@ import { math } from '../../core/math/math.js'; import { Color } from '../../core/math/color.js'; import { RenderPassShaderQuad } from '../../scene/graphics/render-pass-shader-quad.js'; import { shaderChunks } from '../../scene/shader-lib/chunks/chunks.js'; -import { TONEMAP_LINEAR } from '../../scene/constants.js'; -import { ShaderGenerator } from '../../scene/shader-lib/programs/shader-generator.js'; - +import { TONEMAP_LINEAR, tonemapNames } from '../../scene/constants.js'; // Contrast Adaptive Sharpening (CAS) is used to apply the sharpening. It's based on AMD's // FidelityFX CAS, WebGL implementation: https://www.shadertoy.com/view/wtlSWB. It's best to run it @@ -13,6 +11,11 @@ import { ShaderGenerator } from '../../scene/shader-lib/programs/shader-generato // before the tone-mapping. const fragmentShader = /* glsl */ ` + + #include "tonemappingPS" + #include "decodePS" + #include "gamma2_2PS" + varying vec2 uv0; uniform sampler2D sceneTexture; uniform vec2 sceneTextureInvRes; @@ -422,23 +425,24 @@ class RenderPassCompose extends RenderPassShaderQuad { if (this._key !== key) { this._key = key; - const defines = - (this.bloomTexture ? '#define BLOOM\n' : '') + - (this.ssaoTexture ? '#define SSAO\n' : '') + - (this.gradingEnabled ? '#define GRADING\n' : '') + - (this.vignetteEnabled ? '#define VIGNETTE\n' : '') + - (this.fringingEnabled ? '#define FRINGING\n' : '') + - (this.taaEnabled ? '#define TAA\n' : '') + - (this.isSharpnessEnabled ? '#define CAS\n' : '') + - (this._srgb ? '' : '#define GAMMA_CORRECT_OUTPUT\n') + - (this._debug ? `#define DEBUG_COMPOSE ${this._debug}\n` : ''); - - const fsChunks = - shaderChunks.decodePS + - shaderChunks.gamma2_2PS + - ShaderGenerator.tonemapCode(this.toneMapping); - - this.shader = this.createQuadShader(`ComposeShader-${key}`, defines + fsChunks + fragmentShader); + const defines = new Map(); + defines.set('TONEMAP', tonemapNames[this.toneMapping]); + if (this.bloomTexture) defines.set('BLOOM', true); + if (this.ssaoTexture) defines.set('SSAO', true); + if (this.gradingEnabled) defines.set('GRADING', true); + if (this.vignetteEnabled) defines.set('VIGNETTE', true); + if (this.fringingEnabled) defines.set('FRINGING', true); + if (this.taaEnabled) defines.set('TAA', true); + if (this.isSharpnessEnabled) defines.set('CAS', true); + if (!this._srgb) defines.set('GAMMA_CORRECT_OUTPUT', true); + if (this._debug) defines.set('DEBUG_COMPOSE', this._debug); + + const includes = new Map(Object.entries(shaderChunks)); + + this.shader = this.createQuadShader(`ComposeShader-${key}`, fragmentShader, { + fragmentIncludes: includes, + fragmentDefines: defines + }); } } } diff --git a/src/extras/render-passes/render-pass-downsample.js b/src/extras/render-passes/render-pass-downsample.js index e047ef099cb..8cd1d294748 100644 --- a/src/extras/render-passes/render-pass-downsample.js +++ b/src/extras/render-passes/render-pass-downsample.js @@ -61,6 +61,13 @@ class RenderPassDownsample extends RenderPassShaderQuad { this.sourceInvResolutionValue = new Float32Array(2); } + setSourceTexture(value) { + this._sourceTexture = value; + + // change resize source + this.options.resizeSource = value; + } + execute() { this.sourceTextureId.setValue(this.sourceTexture); diff --git a/src/extras/renderers/outline-renderer.js b/src/extras/renderers/outline-renderer.js index f5fdacfdeec..3dbcdc85774 100644 --- a/src/extras/renderers/outline-renderer.js +++ b/src/extras/renderers/outline-renderer.js @@ -97,9 +97,11 @@ class OutlineRenderer { this.outlineShaderPass = this.outlineCameraEntity.camera.setShaderPass('OutlineShaderPass'); // function called after the camera has rendered the outline objects to the texture - this.outlineCameraEntity.camera.onPostRender = () => { - this.onPostRender(); - }; + app.scene.on('postrender', (cameraComponent) => { + if (this.outlineCameraEntity.camera === cameraComponent) { + this.onPostRender(); + } + }); // add the camera to the scene this.app.root.addChild(this.outlineCameraEntity); @@ -331,15 +333,12 @@ class OutlineRenderer { this.updateRenderTarget(sceneCamera); // function called before the scene camera renders a layer - sceneCameraEntity.camera.onPreRenderLayer = (layer, transparent) => { - - // when specified blend layer is rendered, add outline before its rendering - if (transparent === blendLayerTransparent && layer === blendLayer) { + const evt = this.app.scene.on('prerender:layer', (cameraComponent, layer, transparent) => { + if (sceneCamera === cameraComponent && transparent === blendLayerTransparent && layer === blendLayer) { this.blendOutlines(); - - sceneCameraEntity.camera.onPreRenderLayer = null; + evt.off(); } - }; + }); // copy the transform this.outlineCameraEntity.setLocalPosition(sceneCameraEntity.getPosition()); diff --git a/src/framework/asset/asset-reference.js b/src/framework/asset/asset-reference.js index 6e826324a7d..7ece47c005e 100644 --- a/src/framework/asset/asset-reference.js +++ b/src/framework/asset/asset-reference.js @@ -11,6 +11,48 @@ * @category Asset */ class AssetReference { + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLoadById = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtUnloadById = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtAddById = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtRemoveById = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLoadByUrl = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtAddByUrl = null; + + /** + * @type {import('../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtRemoveByUrl = null; + /** * Create a new AssetReference instance. * @@ -109,30 +151,37 @@ class AssetReference { _bind() { if (this.id) { - if (this._onAssetLoad) this._registry.on(`load:${this.id}`, this._onLoad, this); - if (this._onAssetAdd) this._registry.once(`add:${this.id}`, this._onAdd, this); - if (this._onAssetRemove) this._registry.on(`remove:${this.id}`, this._onRemove, this); - if (this._onAssetUnload) this._registry.on(`unload:${this.id}`, this._onUnload, this); + if (this._onAssetLoad) this._evtLoadById = this._registry.on(`load:${this.id}`, this._onLoad, this); + if (this._onAssetAdd) this._evtAddById = this._registry.once(`add:${this.id}`, this._onAdd, this); + if (this._onAssetRemove) this._evtRemoveById = this._registry.on(`remove:${this.id}`, this._onRemove, this); + if (this._onAssetUnload) this._evtUnloadById = this._registry.on(`unload:${this.id}`, this._onUnload, this); } if (this.url) { - if (this._onAssetLoad) this._registry.on(`load:url:${this.url}`, this._onLoad, this); - if (this._onAssetAdd) this._registry.once(`add:url:${this.url}`, this._onAdd, this); - if (this._onAssetRemove) this._registry.on(`remove:url:${this.url}`, this._onRemove, this); + if (this._onAssetLoad) this._evtLoadByUrl = this._registry.on(`load:url:${this.url}`, this._onLoad, this); + if (this._onAssetAdd) this._evtAddByUrl = this._registry.once(`add:url:${this.url}`, this._onAdd, this); + if (this._onAssetRemove) this._evtRemoveByUrl = this._registry.on(`remove:url:${this.url}`, this._onRemove, this); } } _unbind() { if (this.id) { - if (this._onAssetLoad) this._registry.off(`load:${this.id}`, this._onLoad, this); - if (this._onAssetAdd) this._registry.off(`add:${this.id}`, this._onAdd, this); - if (this._onAssetRemove) this._registry.off(`remove:${this.id}`, this._onRemove, this); - if (this._onAssetUnload) this._registry.off(`unload:${this.id}`, this._onUnload, this); + this._evtLoadById?.off(); + this._evtLoadById = null; + this._evtAddById?.off(); + this._evtAddById = null; + this._evtRemoveById?.off(); + this._evtRemoveById = null; + this._evtUnloadById?.off(); + this._evtUnloadById = null; } if (this.url) { - if (this._onAssetLoad) this._registry.off(`load:${this.url}`, this._onLoad, this); - if (this._onAssetAdd) this._registry.off(`add:${this.url}`, this._onAdd, this); - if (this._onAssetRemove) this._registry.off(`remove:${this.url}`, this._onRemove, this); + this._evtLoadByUrl?.off(); + this._evtLoadByUrl = null; + this._evtAddByUrl?.off(); + this._evtAddByUrl = null; + this._evtRemoveByUrl?.off(); + this._evtRemoveByUrl = null; } } diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index c323dc244c3..08d920b1434 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -29,14 +29,6 @@ import { PostEffectQueue } from './post-effect-queue.js'; * @param {number} view - Type of view. Can be {@link VIEW_CENTER}, {@link VIEW_LEFT} or {@link VIEW_RIGHT}. Left and right are only used in stereo rendering. */ -/** - * Callback used by {@link CameraComponent#onPreRenderLayer} and {@link CameraComponent#onPostRenderLayer}. - * - * @callback RenderLayerCallback - * @param {Layer} layer - The layer. - * @param {boolean} transparent - True for transparent sublayer, otherwise opaque sublayer. - */ - /** * The Camera Component enables an Entity to render the scene. A scene requires at least one * enabled camera component to be rendered. Note that multiple camera components can be enabled @@ -69,52 +61,6 @@ class CameraComponent extends Component { */ onPostprocessing = null; - /** - * Custom function that is called before the camera renders the scene. - * - * @type {Function|null} - */ - onPreRender = null; - - /** - * Custom function that is called before the camera renders a layer. This is called during - * rendering to a render target or a default framebuffer, and additional rendering can be - * performed here, for example using ${@link QuadRender#render}. - * - * @type {RenderLayerCallback|null} - */ - onPreRenderLayer = null; - - /** - * Custom function that is called after the camera renders the scene. - * - * @type {Function|null} - */ - onPostRender = null; - - /** - * Custom function that is called after the camera renders a layer. This is called during - * rendering to a render target or a default framebuffer, and additional rendering can be - * performed here, for example using ${@link QuadRender#render}. - * - * @type {RenderLayerCallback|null} - */ - onPostRenderLayer = null; - - /** - * Custom function that is called before visibility culling is performed for this camera. - * - * @type {Function|null} - */ - onPreCull = null; - - /** - * Custom function that is called after visibility culling is performed for this camera. - * - * @type {Function|null} - */ - onPostCull = null; - /** * A counter of requests of depth map rendering. * @@ -151,6 +97,24 @@ class CameraComponent extends Component { /** @private */ _camera = new Camera(); + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Create a new CameraComponent instance. * @@ -1153,16 +1117,20 @@ class CameraComponent extends Component { } onEnable() { - const system = this.system; - const scene = system.app.scene; + const scene = this.system.app.scene; const layers = scene.layers; - system.addCamera(this); + this.system.addCamera(this); + + this._evtLayersChanged?.off(); + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); - scene.on('set:layers', this.onLayersChanged, this); if (layers) { - layers.on('add', this.onLayerAdded, this); - layers.on('remove', this.onLayerRemoved, this); + this._evtLayerAdded?.off(); + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled) { @@ -1173,21 +1141,24 @@ class CameraComponent extends Component { } onDisable() { - const system = this.system; - const scene = system.app.scene; + const scene = this.system.app.scene; const layers = scene.layers; this.postEffects.disable(); this.removeCameraFromLayers(); - scene.off('set:layers', this.onLayersChanged, this); + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; + if (layers) { - layers.off('add', this.onLayerAdded, this); - layers.off('remove', this.onLayerRemoved, this); + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } - system.removeCamera(this); + this.system.removeCamera(this); } onRemove() { diff --git a/src/framework/components/element/component.js b/src/framework/components/element/component.js index b1174a84f0e..6fe42c4c8e2 100644 --- a/src/framework/components/element/component.js +++ b/src/framework/components/element/component.js @@ -218,6 +218,24 @@ class ElementComponent extends Component { */ static EVENT_TOUCHCANCEL = 'touchcancel'; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Create a new ElementComponent instance. * @@ -2530,6 +2548,9 @@ class ElementComponent extends Component { } onEnable() { + const scene = this.system.app.scene; + const layers = scene.layers; + if (this._image) { this._image.onEnable(); } @@ -2544,10 +2565,11 @@ class ElementComponent extends Component { this.system.app.elementInput.addElement(this); } - this.system.app.scene.on('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.on('add', this.onLayerAdded, this); - this.system.app.scene.layers.on('remove', this.onLayerRemoved, this); + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this._batchGroupId >= 0) { @@ -2558,10 +2580,17 @@ class ElementComponent extends Component { } onDisable() { - this.system.app.scene.off('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.off('add', this.onLayerAdded, this); - this.system.app.scene.layers.off('remove', this.onLayerRemoved, this); + const scene = this.system.app.scene; + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; + + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } if (this._image) this._image.onDisable(); diff --git a/src/framework/components/element/image-element.js b/src/framework/components/element/image-element.js index 190b6e80027..0abfc1badbb 100644 --- a/src/framework/components/element/image-element.js +++ b/src/framework/components/element/image-element.js @@ -263,6 +263,12 @@ class ImageRenderable { } class ImageElement { + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtSetMeshes = null; + constructor(element) { this._element = element; this._entity = element.entity; @@ -794,7 +800,7 @@ class ImageElement { // Hook up event handlers on sprite asset _bindSprite(sprite) { - sprite.on('set:meshes', this._onSpriteMeshesChange, this); + this._evtSetMeshes = sprite.on('set:meshes', this._onSpriteMeshesChange, this); sprite.on('set:pixelsPerUnit', this._onSpritePpuChange, this); sprite.on('set:atlas', this._onAtlasTextureChange, this); if (sprite.atlas) { @@ -803,7 +809,8 @@ class ImageElement { } _unbindSprite(sprite) { - sprite.off('set:meshes', this._onSpriteMeshesChange, this); + this._evtSetMeshes?.off(); + this._evtSetMeshes = null; sprite.off('set:pixelsPerUnit', this._onSpritePpuChange, this); sprite.off('set:atlas', this._onAtlasTextureChange, this); if (sprite.atlas) { diff --git a/src/framework/components/gsplat/component.js b/src/framework/components/gsplat/component.js index 431d6896ddd..4ecfd5a1d8b 100644 --- a/src/framework/components/gsplat/component.js +++ b/src/framework/components/gsplat/component.js @@ -52,6 +52,24 @@ class GSplatComponent extends Component { */ _materialOptions = null; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Create a new GSplatComponent. * @@ -324,10 +342,13 @@ class GSplatComponent extends Component { onEnable() { const scene = this.system.app.scene; - scene.on('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.on('add', this.onLayerAdded, this); - scene.layers.on('remove', this.onLayerRemoved, this); + const layers = scene.layers; + + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this._instance) { @@ -339,10 +360,16 @@ class GSplatComponent extends Component { onDisable() { const scene = this.system.app.scene; - scene.off('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.off('add', this.onLayerAdded, this); - scene.layers.off('remove', this.onLayerRemoved, this); + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; + + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } this.removeFromLayers(); diff --git a/src/framework/components/light/component.js b/src/framework/components/light/component.js index e771499ce68..6978628ae11 100644 --- a/src/framework/components/light/component.js +++ b/src/framework/components/light/component.js @@ -42,6 +42,24 @@ import { properties } from './data.js'; * @category Graphics */ class LightComponent extends Component { + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Creates a new LightComponent instance. * @@ -1226,12 +1244,16 @@ class LightComponent extends Component { } onEnable() { + const scene = this.system.app.scene; + const layers = scene.layers; + this.light.enabled = true; - this.system.app.scene.on('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.on('add', this.onLayerAdded, this); - this.system.app.scene.layers.on('remove', this.onLayerRemoved, this); + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled) { @@ -1244,12 +1266,19 @@ class LightComponent extends Component { } onDisable() { + const scene = this.system.app.scene; + const layers = scene.layers; + this.light.enabled = false; - this.system.app.scene.off('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.off('add', this.onLayerAdded, this); - this.system.app.scene.layers.off('remove', this.onLayerRemoved, this); + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; + + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } this.removeLightFromLayers(); diff --git a/src/framework/components/model/component.js b/src/framework/components/model/component.js index 6d9eb4d564b..de46b033e30 100644 --- a/src/framework/components/model/component.js +++ b/src/framework/components/model/component.js @@ -129,6 +129,24 @@ class ModelComponent extends Component { _batchGroup = null; // #endif + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Create a new ModelComponent instance. * @@ -934,11 +952,13 @@ class ModelComponent extends Component { onEnable() { const app = this.system.app; const scene = app.scene; + const layers = scene?.layers; + + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); - scene.on('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.on('add', this.onLayerAdded, this); - scene.layers.on('remove', this.onLayerRemoved, this); + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } const isAsset = (this._type === 'asset'); @@ -985,11 +1005,16 @@ class ModelComponent extends Component { onDisable() { const app = this.system.app; const scene = app.scene; + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; - scene.off('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.off('add', this.onLayerAdded, this); - scene.layers.off('remove', this.onLayerRemoved, this); + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } if (this._batchGroupId >= 0) { diff --git a/src/framework/components/particle-system/component.js b/src/framework/components/particle-system/component.js index ebfef4a01d6..0d310e7fd4d 100644 --- a/src/framework/components/particle-system/component.js +++ b/src/framework/components/particle-system/component.js @@ -112,6 +112,30 @@ class ParticleSystemComponent extends Component { /** @private */ _drawOrder = 0; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtSetMeshes = null; + /** * Create a new ParticleSystemComponent. * @@ -1724,9 +1748,8 @@ class ParticleSystemComponent extends Component { asset.off('unload', this._onRenderAssetUnload, this); asset.off('remove', this._onRenderAssetRemove, this); - if (asset.resource) { - asset.resource.off('set:meshes', this._onRenderSetMeshes, this); - } + this._evtSetMeshes?.off(); + this._evtSetMeshes = null; } _onRenderAssetLoad(asset) { @@ -1747,8 +1770,8 @@ class ParticleSystemComponent extends Component { return; } - render.off('set:meshes', this._onRenderSetMeshes, this); - render.on('set:meshes', this._onRenderSetMeshes, this); + this._evtSetMeshes?.off(); + this._evtSetMeshes = render.on('set:meshes', this._onRenderSetMeshes, this); if (render.meshes) { this._onRenderSetMeshes(render.meshes); @@ -1834,6 +1857,9 @@ class ParticleSystemComponent extends Component { } onEnable() { + const scene = this.system.app.scene; + const layers = scene.layers; + // get data store once const data = this.data; @@ -1955,10 +1981,11 @@ class ParticleSystemComponent extends Component { this.addMeshInstanceToLayers(); } - this.system.app.scene.on('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.on('add', this.onLayerAdded, this); - this.system.app.scene.layers.on('remove', this.onLayerRemoved, this); + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled && data.depthSoftening) { @@ -1967,10 +1994,17 @@ class ParticleSystemComponent extends Component { } onDisable() { - this.system.app.scene.off('set:layers', this.onLayersChanged, this); - if (this.system.app.scene.layers) { - this.system.app.scene.layers.off('add', this.onLayerAdded, this); - this.system.app.scene.layers.off('remove', this.onLayerRemoved, this); + const scene = this.system.app.scene; + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; + + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } if (this.emitter) { diff --git a/src/framework/components/render/component.js b/src/framework/components/render/component.js index 4eb84da5739..8143a7a9d9c 100644 --- a/src/framework/components/render/component.js +++ b/src/framework/components/render/component.js @@ -145,6 +145,30 @@ class RenderComponent extends Component { */ _rootBone; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtSetMeshes = null; + /** * Create a new RenderComponent. * @@ -790,15 +814,17 @@ class RenderComponent extends Component { onEnable() { const app = this.system.app; const scene = app.scene; + const layers = scene.layers; this._rootBone.onParentComponentEnable(); this._cloneSkinInstances(); - scene.on('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.on('add', this.onLayerAdded, this); - scene.layers.on('remove', this.onLayerRemoved, this); + this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } const isAsset = (this._type === 'asset'); @@ -823,11 +849,16 @@ class RenderComponent extends Component { onDisable() { const app = this.system.app; const scene = app.scene; + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; - scene.off('set:layers', this.onLayersChanged, this); - if (scene.layers) { - scene.layers.off('add', this.onLayerAdded, this); - scene.layers.off('remove', this.onLayerRemoved, this); + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } if (this._batchGroupId >= 0) { @@ -881,8 +912,8 @@ class RenderComponent extends Component { if (this._assetReference.asset) { const render = this._assetReference.asset.resource; - render.off('set:meshes', this._onSetMeshes, this); - render.on('set:meshes', this._onSetMeshes, this); + this._evtSetMeshes?.off(); + this._evtSetMeshes = render.on('set:meshes', this._onSetMeshes, this); if (render.meshes) { this._onSetMeshes(render.meshes); } @@ -957,9 +988,8 @@ class RenderComponent extends Component { } _onRenderAssetRemove() { - if (this._assetReference.asset && this._assetReference.asset.resource) { - this._assetReference.asset.resource.off('set:meshes', this._onSetMeshes, this); - } + this._evtSetMeshes?.off(); + this._evtSetMeshes = null; this._onRenderAssetUnload(); } diff --git a/src/framework/components/sprite/component.js b/src/framework/components/sprite/component.js index 2e154ffedfc..8e5c13ae798 100644 --- a/src/framework/components/sprite/component.js +++ b/src/framework/components/sprite/component.js @@ -108,6 +108,24 @@ class SpriteComponent extends Component { */ static EVENT_LOOP = 'loop'; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayersChanged = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerAdded = null; + + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtLayerRemoved = null; + /** * Create a new SpriteComponent instance. * @@ -660,11 +678,13 @@ class SpriteComponent extends Component { onEnable() { const app = this.system.app; const scene = app.scene; + const layers = scene.layers; - scene.on('set:layers', this._onLayersChanged, this); - if (scene.layers) { - scene.layers.on('add', this._onLayerAdded, this); - scene.layers.on('remove', this._onLayerRemoved, this); + this._evtLayersChanged = scene.on('set:layers', this._onLayersChanged, this); + + if (layers) { + this._evtLayerAdded = layers.on('add', this._onLayerAdded, this); + this._evtLayerRemoved = layers.on('remove', this._onLayerRemoved, this); } this._showModel(); @@ -680,11 +700,16 @@ class SpriteComponent extends Component { onDisable() { const app = this.system.app; const scene = app.scene; + const layers = scene.layers; + + this._evtLayersChanged?.off(); + this._evtLayersChanged = null; - scene.off('set:layers', this._onLayersChanged, this); - if (scene.layers) { - scene.layers.off('add', this._onLayerAdded, this); - scene.layers.off('remove', this._onLayerRemoved, this); + if (layers) { + this._evtLayerAdded?.off(); + this._evtLayerAdded = null; + this._evtLayerRemoved?.off(); + this._evtLayerRemoved = null; } this.stop(); diff --git a/src/framework/components/sprite/sprite-animation-clip.js b/src/framework/components/sprite/sprite-animation-clip.js index d4997245ad0..af7e9c134fe 100644 --- a/src/framework/components/sprite/sprite-animation-clip.js +++ b/src/framework/components/sprite/sprite-animation-clip.js @@ -80,6 +80,12 @@ class SpriteAnimationClip extends EventHandler { */ static EVENT_LOOP = 'loop'; + /** + * @type {import('../../../core/event-handle.js').EventHandle|null} + * @private + */ + _evtSetMeshes = null; + /** * Create a new SpriteAnimationClip instance. * @@ -170,7 +176,8 @@ class SpriteAnimationClip extends EventHandler { */ set sprite(value) { if (this._sprite) { - this._sprite.off('set:meshes', this._onSpriteMeshesChange, this); + this._evtSetMeshes?.off(); + this._evtSetMeshes = null; this._sprite.off('set:pixelsPerUnit', this._onSpritePpuChanged, this); this._sprite.off('set:atlas', this._onSpriteMeshesChange, this); if (this._sprite.atlas) { @@ -181,7 +188,7 @@ class SpriteAnimationClip extends EventHandler { this._sprite = value; if (this._sprite) { - this._sprite.on('set:meshes', this._onSpriteMeshesChange, this); + this._evtSetMeshes = this._sprite.on('set:meshes', this._onSpriteMeshesChange, this); this._sprite.on('set:pixelsPerUnit', this._onSpritePpuChanged, this); this._sprite.on('set:atlas', this._onSpriteMeshesChange, this); diff --git a/src/index.js b/src/index.js index 2012b906544..1544fe56495 100644 --- a/src/index.js +++ b/src/index.js @@ -128,6 +128,7 @@ export { BatchGroup } from './scene/batching/batch-group.js'; export { SkinBatchInstance } from './scene/batching/skin-batch-instance.js'; export { BatchManager } from './scene/batching/batch-manager.js'; export { Camera } from './scene/camera.js'; +export { CameraShaderParams } from './scene/camera-shader-params.js'; // needed by the Editor export { WorldClusters } from './scene/lighting/world-clusters.js'; export { ForwardRenderer } from './scene/renderer/forward-renderer.js'; export { GraphNode } from './scene/graph-node.js'; diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js index 7a52d8a1627..547767cc1d1 100644 --- a/src/platform/graphics/constants.js +++ b/src/platform/graphics/constants.js @@ -932,7 +932,7 @@ export const PIXELFORMAT_R16F = 50; export const PIXELFORMAT_RG16F = 51; /** - * 8-bit per-channel unsigned integer (R) format. + * 8-bit per-channel (R) format. * * @type {number} * @category Graphics @@ -940,7 +940,7 @@ export const PIXELFORMAT_RG16F = 51; export const PIXELFORMAT_R8 = 52; /** - * 8-bit per-channel unsigned integer (RG) format. + * 8-bit per-channel (RG) format. * * @type {number} * @category Graphics diff --git a/src/platform/graphics/render-pass.js b/src/platform/graphics/render-pass.js index 1d0e0de29c2..0f952fb6e29 100644 --- a/src/platform/graphics/render-pass.js +++ b/src/platform/graphics/render-pass.js @@ -2,7 +2,7 @@ import { Debug } from '../../core/debug.js'; import { Tracing } from '../../core/tracing.js'; import { Color } from '../../core/math/color.js'; import { TRACEID_RENDER_PASS, TRACEID_RENDER_PASS_DETAIL } from '../../core/constants.js'; -import { pixelFormatInfo } from './constants.js'; +import { isIntegerPixelFormat, pixelFormatInfo } from './constants.js'; /** * @import { GraphicsDevice } from '../graphics/graphics-device.js' @@ -315,8 +315,10 @@ class RenderPass { } // if render target needs mipmaps - if (this.renderTarget?.mipmaps && this.renderTarget?._colorBuffers?.[i].mipmaps) { - colorOps.genMipmaps = true; + const colorBuffer = this.renderTarget?._colorBuffers?.[i]; + if (this.renderTarget?.mipmaps && colorBuffer?.mipmaps) { + const intFormat = isIntegerPixelFormat(colorBuffer._format); + colorOps.genMipmaps = !intFormat; // no automatic mipmap generation for integer formats } } } diff --git a/src/platform/graphics/render-target.js b/src/platform/graphics/render-target.js index 0a4cfbb3cd6..91a3008a53b 100644 --- a/src/platform/graphics/render-target.js +++ b/src/platform/graphics/render-target.js @@ -90,18 +90,6 @@ class RenderTarget { */ _mipmaps; - /** - * @type {number} - * @private - */ - _width; - - /** - * @type {number} - * @private - */ - _height; - /** @type {boolean} */ flipY; @@ -122,8 +110,7 @@ class RenderTarget { * ignored. Texture must have {@link PIXELFORMAT_DEPTH} or {@link PIXELFORMAT_DEPTHSTENCIL} * format. * @param {number} [options.mipLevel] - If set to a number greater than 0, the render target - * will render to the specified mip level of the color buffer. Defaults to 0. Currently only - * supported on WebGPU. + * will render to the specified mip level of the color buffer. Defaults to 0. * @param {number} [options.face] - If the colorBuffer parameter is a cubemap, use this option * to specify the face of the cubemap to render to. Can be: * @@ -253,7 +240,6 @@ class RenderTarget { // if we render to a specific mipmap (even 0), do not generate mipmaps this._mipmaps = options.mipLevel === undefined; - this.updateDimensions(); this.validateMrt(); // device specific implementation @@ -326,13 +312,13 @@ class RenderTarget { */ resize(width, height) { - if (this.mipLevel > 0) { - Debug.warn('Only render target rendering to mipLevel 0 can be resized, ignoring.', this); - return; - } - if (this.width !== width || this.height !== height) { + if (this.mipLevel > 0) { + Debug.warn('Only a render target rendering to mipLevel 0 can be resized, ignoring.', this); + return; + } + // release existing const device = this._device; this.destroyFrameBuffers(); @@ -349,8 +335,6 @@ class RenderTarget { // initialize again this.validateMrt(); this.impl = device.createRenderTargetImpl(this); - - this.updateDimensions(); } } @@ -549,7 +533,11 @@ class RenderTarget { * @type {number} */ get width() { - return this._width ?? this._device.width; + let width = this._colorBuffer?.width || this._depthBuffer?.width || this._device.width; + if (this._mipLevel > 0) { + width = TextureUtils.calcLevelDimension(width, this._mipLevel); + } + return width; } /** @@ -558,17 +546,11 @@ class RenderTarget { * @type {number} */ get height() { - return this._height ?? this._device.height; - } - - updateDimensions() { - this._width = this._colorBuffer?.width ?? this._depthBuffer?.width; - this._height = this._colorBuffer?.height ?? this._depthBuffer?.height; - - if (this._mipLevel > 0 && this._width && this._height) { - this._width = TextureUtils.calcLevelDimension(this._width, this._mipLevel); - this._height = TextureUtils.calcLevelDimension(this._height, this._mipLevel); + let height = this._colorBuffer?.height || this._depthBuffer?.height || this._device.height; + if (this._mipLevel > 0) { + height = TextureUtils.calcLevelDimension(height, this._mipLevel); } + return height; } /** diff --git a/src/platform/graphics/texture.js b/src/platform/graphics/texture.js index 96feba34c2f..94af165cb7f 100644 --- a/src/platform/graphics/texture.js +++ b/src/platform/graphics/texture.js @@ -89,6 +89,12 @@ class Texture { /** @protected */ _storage = false; + /** @protected */ + _numLevels = 0; + + /** @protected */ + _numLevelsRequested; + /** * Create a new Texture instance. * @@ -149,6 +155,9 @@ class Texture { * {@link ADDRESS_REPEAT}. * @param {boolean} [options.mipmaps] - When enabled try to generate or use mipmaps for this * texture. Default is true. + * @param {number} [options.numLevels] - Specifies the number of mip levels to generate. If not + * specified, the number is calculated based on the texture size. When this property is set, + * the mipmaps property is ignored. * @param {boolean} [options.cubemap] - Specifies whether the texture is to be a cubemap. * Defaults to false. * @param {number} [options.arrayLength] - Specifies whether the texture is to be a 2D texture array. @@ -227,7 +236,6 @@ class Texture { this._compressed = isCompressedPixelFormat(this._format); this._integerFormat = isIntegerPixelFormat(this._format); if (this._integerFormat) { - options.mipmaps = false; options.minFilter = FILTER_NEAREST; options.magFilter = FILTER_NEAREST; } @@ -241,7 +249,13 @@ class Texture { this._flipY = options.flipY ?? false; this._premultiplyAlpha = options.premultiplyAlpha ?? false; - this._mipmaps = options.mipmaps ?? options.autoMipmap ?? true; + this._mipmaps = options.mipmaps ?? true; + this._numLevelsRequested = options.numLevels; + if (options.numLevels !== undefined) { + this._numLevels = options.numLevels; + } + this._updateNumLevel(); + this._minFilter = options.minFilter ?? FILTER_LINEAR_MIPMAP_LINEAR; this._magFilter = options.magFilter ?? FILTER_LINEAR; this._anisotropy = options.anisotropy ?? 1; @@ -285,7 +299,7 @@ class Texture { `${this.cubemap ? '[Cubemap]' : ''}` + `${this.volume ? '[Volume]' : ''}` + `${this.array ? '[Array]' : ''}` + - `${this.mipmaps ? '[Mipmaps]' : ''}`, this); + `[MipLevels:${this.numLevels}]`, this); } /** @@ -336,6 +350,7 @@ class Texture { this._width = Math.floor(width); this._height = Math.floor(height); this._depth = Math.floor(depth); + this._updateNumLevel(); // re-create the implementation this.impl = device.createTextureImpl(this); @@ -379,14 +394,16 @@ class Texture { this.renderVersionDirty = this.device.renderVersion; } - /** - * Returns number of required mip levels for the texture based on its dimensions and parameters. - * - * @ignore - * @type {number} - */ - get requiredMipLevels() { - return this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height) : 1; + _updateNumLevel() { + + const maxLevels = this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height) : 1; + const requestedLevels = this._numLevelsRequested; + if (requestedLevels !== undefined && requestedLevels > maxLevels) { + Debug.warn('Texture#numLevels: requested mip level count is greater than the maximum possible, will be clamped to', maxLevels, this); + } + + this._numLevels = Math.min(requestedLevels ?? maxLevels, maxLevels); + this._mipmaps = this._numLevels > 1; } /** @@ -644,6 +661,15 @@ class Texture { return this._mipmaps; } + /** + * Gets the number of mip levels. + * + * @type {number} + */ + get numLevels() { + return this._numLevels; + } + /** * Defines if texture can be used as a storage texture by a compute shader. * diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js index db5f503d74b..fb997b67924 100644 --- a/src/platform/graphics/webgl/webgl-graphics-device.js +++ b/src/platform/graphics/webgl/webgl-graphics-device.js @@ -2059,7 +2059,7 @@ class WebglGraphicsDevice extends GraphicsDevice { renderTarget.destroy(); } resolve(data); - }); + }).catch(reject); }); } diff --git a/src/platform/graphics/webgl/webgl-texture.js b/src/platform/graphics/webgl/webgl-texture.js index 391a69fa3cf..c40c5b43418 100644 --- a/src/platform/graphics/webgl/webgl-texture.js +++ b/src/platform/graphics/webgl/webgl-texture.js @@ -470,7 +470,7 @@ class WebglTexture { let mipObject; let resMult; - const requiredMipLevels = texture.requiredMipLevels; + const requiredMipLevels = texture.numLevels; if (texture.array) { // for texture arrays we reserve the space in advance diff --git a/src/platform/graphics/webgpu/webgpu-texture.js b/src/platform/graphics/webgpu/webgpu-texture.js index d6063ad6ae9..c0c336ce265 100644 --- a/src/platform/graphics/webgpu/webgpu-texture.js +++ b/src/platform/graphics/webgpu/webgpu-texture.js @@ -92,7 +92,7 @@ class WebgpuTexture { const texture = this.texture; const wgpu = device.wgpu; - const mipLevelCount = texture.requiredMipLevels; + const numLevels = texture.numLevels; Debug.assert(texture.width > 0 && texture.height > 0, `Invalid texture dimensions ${texture.width}x${texture.height} for texture ${texture.name}`, texture); @@ -103,7 +103,7 @@ class WebgpuTexture { depthOrArrayLayers: texture.cubemap ? 6 : (texture.array ? texture.arrayLength : 1) }, format: this.format, - mipLevelCount: mipLevelCount, + mipLevelCount: numLevels, sampleCount: 1, dimension: texture.volume ? '3d' : '2d', @@ -300,7 +300,7 @@ class WebgpuTexture { // upload texture data if any let anyUploads = false; let anyLevelMissing = false; - const requiredMipLevels = texture.requiredMipLevels; + const requiredMipLevels = texture.numLevels; for (let mipLevel = 0; mipLevel < requiredMipLevels; mipLevel++) { const mipObject = texture._levels[mipLevel]; @@ -383,7 +383,7 @@ class WebgpuTexture { } } - if (anyUploads && anyLevelMissing && texture.mipmaps && !isCompressedPixelFormat(texture.format)) { + if (anyUploads && anyLevelMissing && texture.mipmaps && !isCompressedPixelFormat(texture.format) && !isIntegerPixelFormat(texture.format)) { device.mipmapRenderer.generate(this); } diff --git a/src/scene/constants.js b/src/scene/constants.js index 917e5fad06e..7665799f7d7 100644 --- a/src/scene/constants.js +++ b/src/scene/constants.js @@ -660,6 +660,17 @@ export const TONEMAP_NEUTRAL = 5; */ export const TONEMAP_NONE = 6; +// names of the tonemaps +export const tonemapNames = [ + 'LINEAR', + 'FILMIC', + 'HEJL', + 'ACES', + 'ACES2', + 'NEUTRAL', + 'NONE' +]; + /** * No specular occlusion. * @@ -1054,3 +1065,51 @@ export const DITHER_BLUENOISE = 'bluenoise'; * @category Graphics */ export const DITHER_IGNNOISE = 'ignnoise'; + +/** + * Name of event fired before the camera renders the scene. + * + * @type {string} + * @ignore + */ +export const EVENT_PRERENDER = 'prerender'; + +/** + * Name of event fired after the camera renders the scene. + * + * @type {string} + * @ignore + */ +export const EVENT_POSTRENDER = 'postrender'; + +/** + * Name of event fired before a layer is rendered by a camera. + * + * @type {string} + * @ignore + */ +export const EVENT_PRERENDER_LAYER = 'prerender:layer'; + +/** + * Name of event fired after a layer is rendered by a camera. + * + * @type {string} + * @ignore + */ +export const EVENT_POSTRENDER_LAYER = 'postrender:layer'; + +/** + * Name of event fired before visibility culling is performed for the camera + * + * @type {string} + * @ignore + */ +export const EVENT_PRECULL = 'precull'; + +/** + * Name of event after before visibility culling is performed for the camera + * + * @type {string} + * @ignore + */ +export const EVENT_POSTCULL = 'postcull'; diff --git a/src/scene/gsplat/gsplat-compressed-material.js b/src/scene/gsplat/gsplat-compressed-material.js deleted file mode 100644 index 38efeac4e32..00000000000 --- a/src/scene/gsplat/gsplat-compressed-material.js +++ /dev/null @@ -1,533 +0,0 @@ -import { CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; -import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js'; -import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE, TONEMAP_LINEAR } from '../constants.js'; -import { ShaderMaterial } from '../materials/shader-material.js'; -import { getProgramLibrary } from '../shader-lib/get-program-library.js'; - -import { hashCode } from '../../core/hash.js'; -import { ShaderUtils } from '../../platform/graphics/shader-utils.js'; -import { shaderChunks } from '../shader-lib/chunks/chunks.js'; -import { ShaderGenerator } from '../shader-lib/programs/shader-generator.js'; -import { ShaderPass } from '../shader-pass.js'; -import { getMaterialShaderDefines } from '../shader-lib/utils.js'; - -const splatCoreVS = /* glsl */ ` - uniform mat4 matrix_model; - uniform mat4 matrix_view; - uniform mat4 matrix_projection; - - uniform vec2 viewport; - uniform vec4 tex_params; // num splats, packed width, chunked width - - uniform highp usampler2D splatOrder; - uniform highp usampler2D packedTexture; - uniform highp sampler2D chunkTexture; - - attribute vec3 vertex_position; - attribute uint vertex_id_attrib; - - #ifndef DITHER_NONE - varying float id; - #endif - - uint orderId; - uint splatId; - ivec2 packedUV; - ivec2 chunkUV; - - vec4 chunkDataA; // x: min_x, y: min_y, z: min_z, w: max_x - vec4 chunkDataB; // x: max_y, y: max_z, z: scale_min_x, w: scale_min_y - vec4 chunkDataC; // x: scale_min_z, y: scale_max_x, z: scale_max_y, w: scale_max_z - vec4 chunkDataD; // x: min_r, y: min_g, z: min_b, w: max_r - vec4 chunkDataE; // x: max_g, y: max_b, z: unused, w: unused - uvec4 packedData; // x: position bits, y: rotation bits, z: scale bits, w: color bits - - // calculate the current splat index and uvs - bool calcSplatUV() { - uint numSplats = uint(tex_params.x); - uint packedWidth = uint(tex_params.y); - uint chunkWidth = uint(tex_params.z); - - // calculate splat index - orderId = vertex_id_attrib + uint(vertex_position.z); - - // checkout for out of bounds - if (orderId >= numSplats) { - return false; - } - - // calculate packedUV - ivec2 orderUV = ivec2( - int(orderId % packedWidth), - int(orderId / packedWidth) - ); - splatId = texelFetch(splatOrder, orderUV, 0).r; - packedUV = ivec2( - int(splatId % packedWidth), - int(splatId / packedWidth) - ); - - // calculate chunkUV - uint chunkId = splatId / 256u; - chunkUV = ivec2( - int((chunkId % chunkWidth) * 5u), - int(chunkId / chunkWidth) - ); - - return true; - } - - // read chunk and packed data from textures - void readData() { - chunkDataA = texelFetch(chunkTexture, chunkUV, 0); - chunkDataB = texelFetch(chunkTexture, ivec2(chunkUV.x + 1, chunkUV.y), 0); - chunkDataC = texelFetch(chunkTexture, ivec2(chunkUV.x + 2, chunkUV.y), 0); - chunkDataD = texelFetch(chunkTexture, ivec2(chunkUV.x + 3, chunkUV.y), 0); - chunkDataE = texelFetch(chunkTexture, ivec2(chunkUV.x + 4, chunkUV.y), 0); - packedData = texelFetch(packedTexture, packedUV, 0); - } - - vec3 unpack111011(uint bits) { - return vec3( - float(bits >> 21u) / 2047.0, - float((bits >> 11u) & 0x3ffu) / 1023.0, - float(bits & 0x7ffu) / 2047.0 - ); - } - - vec4 unpack8888(uint bits) { - return vec4( - float(bits >> 24u) / 255.0, - float((bits >> 16u) & 0xffu) / 255.0, - float((bits >> 8u) & 0xffu) / 255.0, - float(bits & 0xffu) / 255.0 - ); - } - - float norm = 1.0 / (sqrt(2.0) * 0.5); - - vec4 unpackRotation(uint bits) { - float a = (float((bits >> 20u) & 0x3ffu) / 1023.0 - 0.5) * norm; - float b = (float((bits >> 10u) & 0x3ffu) / 1023.0 - 0.5) * norm; - float c = (float(bits & 0x3ffu) / 1023.0 - 0.5) * norm; - float m = sqrt(1.0 - (a * a + b * b + c * c)); - - uint mode = bits >> 30u; - if (mode == 0u) return vec4(m, a, b, c); - if (mode == 1u) return vec4(a, m, b, c); - if (mode == 2u) return vec4(a, b, m, c); - return vec4(a, b, c, m); - } - - vec3 getCenter() { - return mix(chunkDataA.xyz, vec3(chunkDataA.w, chunkDataB.xy), unpack111011(packedData.x)); - } - - vec4 getRotation() { - return unpackRotation(packedData.y); - } - - vec3 getScale() { - return exp(mix(vec3(chunkDataB.zw, chunkDataC.x), chunkDataC.yzw, unpack111011(packedData.z))); - } - - vec4 getColor() { - vec4 r = unpack8888(packedData.w); - return vec4(mix(chunkDataD.xyz, vec3(chunkDataD.w, chunkDataE.xy), r.rgb), r.w); - } - - mat3 quatToMat3(vec4 R) { - float x = R.x; - float y = R.y; - float z = R.z; - float w = R.w; - return mat3( - 1.0 - 2.0 * (z * z + w * w), - 2.0 * (y * z + x * w), - 2.0 * (y * w - x * z), - 2.0 * (y * z - x * w), - 1.0 - 2.0 * (y * y + w * w), - 2.0 * (z * w + x * y), - 2.0 * (y * w + x * z), - 2.0 * (z * w - x * y), - 1.0 - 2.0 * (y * y + z * z) - ); - } - - // Given a rotation matrix and scale vector, compute 3d covariance A and B - void getCovariance(out vec3 covA, out vec3 covB) { - mat3 rot = quatToMat3(getRotation()); - vec3 scale = getScale(); - - // M = S * R - mat3 M = transpose(mat3( - scale.x * rot[0], - scale.y * rot[1], - scale.z * rot[2] - )); - - covA = vec3(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2])); - covB = vec3(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2])); - } - - // calculate 2d covariance vectors - vec4 calcV1V2(in vec3 splat_cam, in vec3 covA, in vec3 covB, mat3 W) { - mat3 Vrk = mat3( - covA.x, covA.y, covA.z, - covA.y, covB.x, covB.y, - covA.z, covB.y, covB.z - ); - - float focal = viewport.x * matrix_projection[0][0]; - - float J1 = focal / splat_cam.z; - vec2 J2 = -J1 / splat_cam.z * splat_cam.xy; - mat3 J = mat3( - J1, 0.0, J2.x, - 0.0, J1, J2.y, - 0.0, 0.0, 0.0 - ); - - mat3 T = W * J; - mat3 cov = transpose(T) * Vrk * T; - - float diagonal1 = cov[0][0] + 0.3; - float offDiagonal = cov[0][1]; - float diagonal2 = cov[1][1] + 0.3; - - float mid = 0.5 * (diagonal1 + diagonal2); - float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); - float lambda1 = mid + radius; - float lambda2 = max(mid - radius, 0.1); - vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); - - vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector; - vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x); - - return vec4(v1, v2); - } - -#if defined(USE_SH) - #define SH_C1 0.4886025119029199f - - #define SH_C2_0 1.0925484305920792f - #define SH_C2_1 -1.0925484305920792f - #define SH_C2_2 0.31539156525252005f - #define SH_C2_3 -1.0925484305920792f - #define SH_C2_4 0.5462742152960396f - - #define SH_C3_0 -0.5900435899266435f - #define SH_C3_1 2.890611442640554f - #define SH_C3_2 -0.4570457994644658f - #define SH_C3_3 0.3731763325901154f - #define SH_C3_4 -0.4570457994644658f - #define SH_C3_5 1.445305721320277f - #define SH_C3_6 -0.5900435899266435f - - uniform highp usampler2D shTexture0; - uniform highp usampler2D shTexture1; - uniform highp usampler2D shTexture2; - - vec4 sunpack8888(in uint bits) { - return vec4((uvec4(bits) >> uvec4(0u, 8u, 16u, 24u)) & 0xffu) * (8.0 / 255.0) - 4.0; - } - - void readSHData(out vec3 sh[15]) { - // read the sh coefficients - uvec4 shData0 = texelFetch(shTexture0, packedUV, 0); - uvec4 shData1 = texelFetch(shTexture1, packedUV, 0); - uvec4 shData2 = texelFetch(shTexture2, packedUV, 0); - - vec4 r0 = sunpack8888(shData0.x); - vec4 r1 = sunpack8888(shData0.y); - vec4 r2 = sunpack8888(shData0.z); - vec4 r3 = sunpack8888(shData0.w); - - vec4 g0 = sunpack8888(shData1.x); - vec4 g1 = sunpack8888(shData1.y); - vec4 g2 = sunpack8888(shData1.z); - vec4 g3 = sunpack8888(shData1.w); - - vec4 b0 = sunpack8888(shData2.x); - vec4 b1 = sunpack8888(shData2.y); - vec4 b2 = sunpack8888(shData2.z); - vec4 b3 = sunpack8888(shData2.w); - - sh[0] = vec3(r0.x, g0.x, b0.x); - sh[1] = vec3(r0.y, g0.y, b0.y); - sh[2] = vec3(r0.z, g0.z, b0.z); - sh[3] = vec3(r0.w, g0.w, b0.w); - sh[4] = vec3(r1.x, g1.x, b1.x); - sh[5] = vec3(r1.y, g1.y, b1.y); - sh[6] = vec3(r1.z, g1.z, b1.z); - sh[7] = vec3(r1.w, g1.w, b1.w); - sh[8] = vec3(r2.x, g2.x, b2.x); - sh[9] = vec3(r2.y, g2.y, b2.y); - sh[10] = vec3(r2.z, g2.z, b2.z); - sh[11] = vec3(r2.w, g2.w, b2.w); - sh[12] = vec3(r3.x, g3.x, b3.x); - sh[13] = vec3(r3.y, g3.y, b3.y); - sh[14] = vec3(r3.z, g3.z, b3.z); - } - - // see https://github.com/graphdeco-inria/gaussian-splatting/blob/main/utils/sh_utils.py - vec3 evalSH(in vec3 dir) { - - vec3 sh[15]; - readSHData(sh); - - vec3 result = vec3(0.0); - - // 1st degree - float x = dir.x; - float y = dir.y; - float z = dir.z; - - result += SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x); - - // 2nd degree - float xx = x * x; - float yy = y * y; - float zz = z * z; - float xy = x * y; - float yz = y * z; - float xz = x * z; - - result += - sh[3] * (SH_C2_0 * xy) * + - sh[4] * (SH_C2_1 * yz) + - sh[5] * (SH_C2_2 * (2.0 * zz - xx - yy)) + - sh[6] * (SH_C2_3 * xz) + - sh[7] * (SH_C2_4 * (xx - yy)); - - // 3rd degree - result += - sh[8] * (SH_C3_0 * y * (3.0 * xx - yy)) + - sh[9] * (SH_C3_1 * xy * z) + - sh[10] * (SH_C3_2 * y * (4.0 * zz - xx - yy)) + - sh[11] * (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) + - sh[12] * (SH_C3_4 * x * (4.0 * zz - xx - yy)) + - sh[13] * (SH_C3_5 * z * (xx - yy)) + - sh[14] * (SH_C3_6 * x * (xx - 3.0 * yy)); - - return result; - } -#else - vec3 evalSH(in vec3 dir) { - return vec3(0.0); - } -#endif -`; - -const splatCoreFS = /* glsl */ ` - #ifndef DITHER_NONE - varying float id; - #endif - - #ifdef PICK_PASS - uniform vec4 uColor; - #endif - - vec4 evalSplat(vec2 texCoord, vec4 color) { - mediump float A = dot(texCoord, texCoord); - if (A > 1.0) { - discard; - } - - mediump float B = exp(-A * 4.0) * color.a; - if (B < 1.0 / 255.0) { - discard; - } - - #ifdef PICK_PASS - if (B < 0.3) { - discard; - } - return uColor; - #endif - - #ifndef DITHER_NONE - opacityDither(B, id * 0.013); - #endif - - #ifdef TONEMAP_ENABLED - return vec4(gammaCorrectOutput(toneMap(decodeGamma(color.rgb))), B); - #else - return vec4(color.rgb, B); - #endif - } -`; - -class GSplatCompressedShaderGenerator { - generateKey(options) { - const vsHash = hashCode(options.vertex); - const fsHash = hashCode(options.fragment); - const definesHash = ShaderGenerator.definesHash(options.defines); - return `splat-${options.pass}-${options.gamma}-${options.toneMapping}-${vsHash}-${fsHash}-${options.dither}-${definesHash}}`; - } - - createShaderDefinition(device, options) { - - const shaderPassInfo = ShaderPass.get(device).getByIndex(options.pass); - const shaderPassDefines = shaderPassInfo.shaderDefines; - - const defines = - `${shaderPassDefines}\n` + - `#define DITHER_${options.dither.toUpperCase()}\n` + - `#define TONEMAP_${options.toneMapping === TONEMAP_LINEAR ? 'DISABLED' : 'ENABLED'}\n`; - - const vs = defines + splatCoreVS + options.vertex; - const fs = defines + shaderChunks.decodePS + - (options.dither === DITHER_NONE ? '' : shaderChunks.bayerPS + shaderChunks.opacityDitherPS) + - ShaderGenerator.tonemapCode(options.toneMapping) + - ShaderGenerator.gammaCode(options.gamma) + - splatCoreFS + options.fragment; - - const defineMap = new Map(); - options.defines.forEach((value, key) => { - defineMap.set(key, value); - }); - - return ShaderUtils.createDefinition(device, { - name: 'SplatShader', - attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_id_attrib: SEMANTIC_ATTR13 - }, - vertexCode: vs, - fragmentCode: fs, - fragmentDefines: defineMap, - vertexDefines: defineMap - }); - } -} - -const gsplatCompressed = new GSplatCompressedShaderGenerator(); - -const splatMainVS = /* glsl */ ` - varying mediump vec2 texCoord; - varying mediump vec4 color; - - uniform vec3 view_position; - - mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); - - void main(void) - { - // calculate splat uv - if (!calcSplatUV()) { - gl_Position = discardVec; - return; - } - - // read chunk data and packed data - readData(); - - // get center - vec3 center = getCenter(); - - // handle transforms - mat4 model_view = matrix_view * matrix_model; - vec4 splat_cam = model_view * vec4(center, 1.0); - vec4 splat_proj = matrix_projection * splat_cam; - - // cull behind camera - if (splat_proj.z < -splat_proj.w) { - gl_Position = discardVec; - return; - } - - // get covariance - vec3 covA, covB; - getCovariance(covA, covB); - - vec4 v1v2 = calcV1V2(splat_cam.xyz, covA, covB, transpose(mat3(model_view))); - - // get color - color = getColor(); - - // calculate scale based on alpha - float scale = min(1.0, sqrt(-log(1.0 / 255.0 / color.a)) / 2.0); - - v1v2 *= scale; - - // early out tiny splats - if (dot(v1v2.xy, v1v2.xy) < 4.0 && dot(v1v2.zw, v1v2.zw) < 4.0) { - gl_Position = discardVec; - return; - } - - gl_Position = splat_proj + vec4((vertex_position.x * v1v2.xy + vertex_position.y * v1v2.zw) / viewport * splat_proj.w, 0, 0); - - texCoord = vertex_position.xy * scale / 2.0; - - #ifdef USE_SH - vec4 worldCenter = matrix_model * vec4(center, 1.0); - vec3 viewDir = normalize((worldCenter.xyz / worldCenter.w - view_position) * mat3(matrix_model)); - color.xyz = max(color.xyz + evalSH(viewDir), 0.0); - #endif - - #ifndef DITHER_NONE - id = float(splatId); - #endif - } -`; - -const splatMainFS = /* glsl */ ` - varying mediump vec2 texCoord; - varying mediump vec4 color; - - void main(void) - { - gl_FragColor = evalSplat(texCoord, color); - } -`; - -/** - * @typedef {object} SplatMaterialOptions - The options. - * @property {string} [vertex] - Custom vertex shader, see SPLAT MANY example. - * @property {string} [fragment] - Custom fragment shader, see SPLAT MANY example. - * @property {string} [dither] - Opacity dithering enum. - */ - -/** - * @param {SplatMaterialOptions} [options] - The options. - * @returns {Material} The GS material. - */ -const createGSplatCompressedMaterial = (options = {}) => { - - const ditherEnum = options.dither ?? DITHER_NONE; - const dither = ditherEnum !== DITHER_NONE; - - const material = new ShaderMaterial(); - material.name = 'compressedSplatMaterial'; - material.cull = CULLFACE_NONE; - material.blendType = dither ? BLEND_NONE : BLEND_NORMAL; - material.depthWrite = dither; - - material.getShaderVariant = function (params) { - - const { cameraShaderParams } = params; - const programOptions = { - defines: getMaterialShaderDefines(material, cameraShaderParams), - pass: params.pass, - gamma: params.cameraShaderParams.shaderOutputGamma, - toneMapping: params.cameraShaderParams.toneMapping, - vertex: options.vertex ?? splatMainVS, - fragment: options.fragment ?? splatMainFS, - dither: ditherEnum - }; - - const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat); - - const library = getProgramLibrary(params.device); - library.register('splat-compressed', gsplatCompressed); - return library.getProgram('splat-compressed', programOptions, processingOptions); - }; - - material.update(); - - return material; -}; - -export { createGSplatCompressedMaterial }; diff --git a/src/scene/gsplat/gsplat-compressed.js b/src/scene/gsplat/gsplat-compressed.js index dace17c067a..402f88a0035 100644 --- a/src/scene/gsplat/gsplat-compressed.js +++ b/src/scene/gsplat/gsplat-compressed.js @@ -4,7 +4,7 @@ import { BoundingBox } from '../../core/shape/bounding-box.js'; import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA32U } from '../../platform/graphics/constants.js'; -import { createGSplatCompressedMaterial } from './gsplat-compressed-material.js'; +import { createGSplatMaterial } from './gsplat-material.js'; /** * @import { GSplatCompressedData } from './gsplat-compressed-data.js' @@ -27,6 +27,8 @@ class GSplatCompressed { numSplats; + numSplatsVisible; + /** @type {BoundingBox} */ aabb; @@ -57,6 +59,7 @@ class GSplatCompressed { this.device = device; this.numSplats = numSplats; + this.numVisibleSplats = numSplats; // initialize aabb this.aabb = new BoundingBox(); @@ -109,10 +112,10 @@ class GSplatCompressed { const srcCoeffs = [3, 8, 15][shBands - 1]; for (let i = 0; i < numSplats; ++i) { - for (let j = 0; j < srcCoeffs; ++j) { - target0[i * 16 + j] = shData[(i * 3 + 0) * srcCoeffs + j]; - target1[i * 16 + j] = shData[(i * 3 + 1) * srcCoeffs + j]; - target2[i * 16 + j] = shData[(i * 3 + 2) * srcCoeffs + j]; + for (let j = 0; j < 15; ++j) { + target0[i * 16 + j] = j < srcCoeffs ? shData[(i * 3 + 0) * srcCoeffs + j] : 127; + target1[i * 16 + j] = j < srcCoeffs ? shData[(i * 3 + 1) * srcCoeffs + j] : 127; + target2[i * 16 + j] = j < srcCoeffs ? shData[(i * 3 + 2) * srcCoeffs + j] : 127; } } @@ -143,15 +146,18 @@ class GSplatCompressed { * @returns {Material} material - The material to set up for the splat rendering. */ createMaterial(options) { - const result = createGSplatCompressedMaterial(options); + const result = createGSplatMaterial(options); + result.setDefine('GSPLAT_COMPRESSED_DATA', true); result.setParameter('packedTexture', this.packedTexture); result.setParameter('chunkTexture', this.chunkTexture); - result.setParameter('tex_params', new Float32Array([this.numSplats, this.packedTexture.width, this.chunkTexture.width / 5, 0])); + result.setParameter('numSplats', this.numSplatsVisible); if (this.shTexture0) { - result.setDefine('USE_SH', true); + result.setDefine('SH_BANDS', 3); result.setParameter('shTexture0', this.shTexture0); result.setParameter('shTexture1', this.shTexture1); result.setParameter('shTexture2', this.shTexture2); + } else { + result.setDefine('SH_BANDS', 0); } return result; } diff --git a/src/scene/gsplat/gsplat-instance.js b/src/scene/gsplat/gsplat-instance.js index 5a0c0a0c01b..fcaa67b4078 100644 --- a/src/scene/gsplat/gsplat-instance.js +++ b/src/scene/gsplat/gsplat-instance.js @@ -104,10 +104,10 @@ class GSplatInstance { const meshIndices = new Uint32Array(6 * splatInstanceSize); for (let i = 0; i < splatInstanceSize; ++i) { meshPositions.set([ - -2, -2, i, - 2, -2, i, - 2, 2, i, - -2, 2, i + -1, -1, i, + 1, -1, i, + 1, 1, i, + -1, 1, i ], i * 12); const b = i * 4; @@ -143,10 +143,7 @@ class GSplatInstance { this.meshInstance.instancingCount = Math.ceil(count / splatInstanceSize); // update splat count on the material - const tex_params = this.material.getParameter('tex_params'); - if (tex_params?.data) { - tex_params.data[0] = count; - } + this.material.setParameter('numSplats', count); }); } } diff --git a/src/scene/gsplat/gsplat-material.js b/src/scene/gsplat/gsplat-material.js index 9038bbe6161..60e79f72397 100644 --- a/src/scene/gsplat/gsplat-material.js +++ b/src/scene/gsplat/gsplat-material.js @@ -1,102 +1,88 @@ -import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js'; -import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE } from '../constants.js'; +import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE, GAMMA_NONE, GAMMA_SRGB, tonemapNames } from '../constants.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { getProgramLibrary } from '../shader-lib/get-program-library.js'; import { getMaterialShaderDefines } from '../shader-lib/utils.js'; -import { gsplat } from './shader-generator-gsplat.js'; - -const splatMainVS = /* glsl */ ` - uniform vec3 view_position; - - uniform sampler2D splatColor; - - varying mediump vec2 texCoord; - varying mediump vec4 color; - - mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); - - void main(void) - { - // calculate splat uv - if (!calcSplatUV()) { - gl_Position = discardVec; - return; - } - - // get center - vec3 center = getCenter(); - - // handle transforms - mat4 model_view = matrix_view * matrix_model; - vec4 splat_cam = model_view * vec4(center, 1.0); - vec4 splat_proj = matrix_projection * splat_cam; - - // cull behind camera - if (splat_proj.z < -splat_proj.w) { - gl_Position = discardVec; - return; - } - - // get covariance - vec3 covA, covB; - getCovariance(covA, covB); - - vec4 v1v2 = calcV1V2(splat_cam.xyz, covA, covB, transpose(mat3(model_view))); - - // get color - color = texelFetch(splatColor, splatUV, 0); - - // calculate scale based on alpha - float scale = min(1.0, sqrt(-log(1.0 / 255.0 / color.a)) / 2.0); - - v1v2 *= scale; - - // early out tiny splats - if (dot(v1v2.xy, v1v2.xy) < 4.0 && dot(v1v2.zw, v1v2.zw) < 4.0) { - gl_Position = discardVec; - return; - } - - gl_Position = splat_proj + vec4((vertex_position.x * v1v2.xy + vertex_position.y * v1v2.zw) / viewport * splat_proj.w, 0, 0); - - texCoord = vertex_position.xy * scale / 2.0; +import { ShaderUtils } from '../../platform/graphics/shader-utils.js'; +import { shaderChunks } from '../shader-lib/chunks/chunks.js'; +import { ShaderGenerator } from '../shader-lib/programs/shader-generator.js'; +import { ShaderPass } from '../shader-pass.js'; +import { hashCode } from '../../core/hash.js'; + +const gammaNames = { + [GAMMA_NONE]: 'NONE', + [GAMMA_SRGB]: 'SRGB' +}; - #ifdef USE_SH1 - vec4 worldCenter = matrix_model * vec4(center, 1.0); - vec3 viewDir = normalize((worldCenter.xyz / worldCenter.w - view_position) * mat3(matrix_model)); - color.xyz = max(color.xyz + evalSH(viewDir), 0.0); - #endif +const defaultChunks = new Map(Object.entries(shaderChunks)); - #ifndef DITHER_NONE - id = float(splatId); - #endif +class GSplatShaderGenerator { + generateKey(options) { + const { pass, gamma, toneMapping, vertex, fragment, dither, defines, chunks } = options; + return `splat-${pass}-${gamma}-${toneMapping}-${hashCode(vertex)}-${hashCode(fragment)}-${dither}-${ShaderGenerator.definesHash(defines)}-${chunks && Object.keys(chunks).sort().join(':')}`; } -`; - -const splatMainFS = /* glsl */ ` - varying mediump vec2 texCoord; - varying mediump vec4 color; - void main(void) - { - gl_FragColor = evalSplat(texCoord, color); + createShaderDefinition(device, options) { + const shaderPassInfo = ShaderPass.get(device).getByIndex(options.pass); + const shaderPassDefines = shaderPassInfo.shaderDefines; + + const defineMap = new Map(); + + // define tonemap + defineMap.set('TONEMAP', tonemapNames[options.toneMapping] ?? true); + + // define gamma + defineMap.set('GAMMA', gammaNames[options.gamma] ?? true); + + // it would nice if DITHER type was defined like the others + defineMap.set(`DITHER_${options.dither.toUpperCase()}`, true); + + // add user defines + options.defines.forEach((value, key) => { + defineMap.set(key, value); + }); + + const defines = `${shaderPassDefines}\n`; + const vs = defines + (options.vertex ?? shaderChunks.gsplatVS); + const fs = defines + (options.fragment ?? shaderChunks.gsplatPS); + const includes = options.chunks ? new Map(Object.entries({ + ...shaderChunks, + ...options.chunks + })) : defaultChunks; + + return ShaderUtils.createDefinition(device, { + name: 'SplatShader', + attributes: { + vertex_position: SEMANTIC_POSITION, + vertex_id_attrib: SEMANTIC_ATTR13 + }, + vertexCode: vs, + vertexDefines: defineMap, + vertexIncludes: includes, + fragmentCode: fs, + fragmentDefines: defineMap, + fragmentIncludes: includes + }); } -`; +} + +const gsplat = new GSplatShaderGenerator(); /** * @typedef {object} SplatMaterialOptions - The options. * @property {string} [vertex] - Custom vertex shader, see SPLAT MANY example. * @property {string} [fragment] - Custom fragment shader, see SPLAT MANY example. - * @property {string} [dither] - Opacity dithering enum. * @property {string[]} [defines] - List of shader defines. + * @property {Object} [chunks] - Custom shader chunks. + * @property {string} [dither] - Opacity dithering enum. * * @ignore */ /** * @param {SplatMaterialOptions} [options] - The options. - * @returns {Material} The GS material. + * @returns {ShaderMaterial} The GS material. */ const createGSplatMaterial = (options = {}) => { @@ -117,8 +103,9 @@ const createGSplatMaterial = (options = {}) => { pass: params.pass, gamma: cameraShaderParams.shaderOutputGamma, toneMapping: cameraShaderParams.toneMapping, - vertex: options.vertex ?? splatMainVS, - fragment: options.fragment ?? splatMainFS, + vertex: options.vertex, + fragment: options.fragment, + chunks: options.chunks, dither: ditherEnum }; diff --git a/src/scene/gsplat/gsplat-sorter.js b/src/scene/gsplat/gsplat-sorter.js index bdecc363977..539a0012423 100644 --- a/src/scene/gsplat/gsplat-sorter.js +++ b/src/scene/gsplat/gsplat-sorter.js @@ -3,15 +3,6 @@ import { TEXTURELOCK_READ } from '../../platform/graphics/constants.js'; // sort blind set of data function SortWorker() { - - // number of bits used to store the distance in integer array. Smaller number gives it a smaller - // precision but faster sorting. Could be dynamic for less precise sorting. - // 16bit seems plenty of large scenes (train), 10bits is enough for sled. - const compareBits = 16; - - // number of buckets for count sorting to represent each unique distance using compareBits bits - const bucketCount = (2 ** compareBits) + 1; - let order; let centers; let mapping; @@ -45,7 +36,7 @@ function SortWorker() { }; const update = () => { - if (!order || !centers || !cameraPosition || !cameraDirection) return; + if (!order || !centers || centers.length === 0 || !cameraPosition || !cameraDirection) return; const px = cameraPosition.x; const py = cameraPosition.y; @@ -75,12 +66,6 @@ function SortWorker() { lastCameraDirection.y = dy; lastCameraDirection.z = dz; - // create distance buffer - const numVertices = centers.length / 3; - if (distances?.length !== numVertices) { - distances = new Uint32Array(numVertices); - } - // calc min/max distance using bound let minDist; let maxDist; @@ -97,7 +82,18 @@ function SortWorker() { } } - if (!countBuffer) { + const numVertices = centers.length / 3; + + // calculate number of bits needed to store sorting result + const compareBits = Math.max(10, Math.min(20, Math.round(Math.log2(numVertices / 4)))); + const bucketCount = 2 ** compareBits + 1; + + // create distance buffer + if (distances?.length !== numVertices) { + distances = new Uint32Array(numVertices); + } + + if (!countBuffer || countBuffer.length !== bucketCount) { countBuffer = new Uint32Array(bucketCount); } else { countBuffer.fill(0); diff --git a/src/scene/gsplat/gsplat.js b/src/scene/gsplat/gsplat.js index a7a16fa530b..d9673f276c1 100644 --- a/src/scene/gsplat/gsplat.js +++ b/src/scene/gsplat/gsplat.js @@ -31,6 +31,8 @@ class GSplat { numSplats; + numSplatsVisible; + /** @type {Float32Array} */ centers; @@ -70,6 +72,7 @@ class GSplat { this.device = device; this.numSplats = numSplats; + this.numSplatsVisible = numSplats; this.centers = new Float32Array(gsplatData.numSplats * 3); gsplatData.getCenters(this.centers); @@ -116,15 +119,15 @@ class GSplat { result.setParameter('splatColor', this.colorTexture); result.setParameter('transformA', this.transformATexture); result.setParameter('transformB', this.transformBTexture); - result.setParameter('tex_params', new Float32Array([this.numSplats, this.colorTexture.width, 0, 0])); + result.setParameter('numSplats', this.numSplatsVisible); if (this.hasSH) { - result.setDefine('USE_SH1', true); - result.setDefine('USE_SH2', true); - result.setDefine('USE_SH3', true); + result.setDefine('SH_BANDS', 3); result.setParameter('splatSH_1to3', this.sh1to3Texture); result.setParameter('splatSH_4to7', this.sh4to7Texture); result.setParameter('splatSH_8to11', this.sh8to11Texture); result.setParameter('splatSH_12to15', this.sh12to15Texture); + } else { + result.setDefine('SH_BANDS', 0); } return result; } @@ -289,15 +292,6 @@ class GSplat { const t11 = (1 << 11) - 1; const t10 = (1 << 10) - 1; - const pack = (r, g, b) => { - const rb = Math.floor(r * t11 + 0.5); - const gb = Math.floor(g * t10 + 0.5); - const bb = Math.floor(b * t11 + 0.5); - - return (rb < 0 ? 0 : rb > t11 ? t11 : rb) << 21 | - (gb < 0 ? 0 : gb > t10 ? t10 : gb) << 11 | - (bb < 0 ? 0 : bb > t11 ? t11 : bb); - }; const float32 = new Float32Array(1); const uint32 = new Uint32Array(float32.buffer); @@ -310,49 +304,52 @@ class GSplat { ]; for (let i = 0; i < gsplatData.numSplats; ++i) { - // get coefficients - for (let j = 0; j < 45; ++j) { - c[j] = src[j][i]; + // extract coefficients + for (let j = 0; j < 15; ++j) { + c[j * 3] = src[j][i]; + c[j * 3 + 1] = src[j + 15][i]; + c[j * 3 + 2] = src[j + 30][i]; } - // calculate maximum absolute value - let m = Math.abs(c[0]); + // calc maximum value + let max = c[0]; for (let j = 1; j < 45; ++j) { - const as = Math.abs(c[j]); - if (as > m) m = as; + max = Math.max(max, Math.abs(c[j])); } - if (m === 0) { + if (max === 0) { continue; } // normalize - for (let j = 0; j < 45; ++j) { - c[j] = (c[j] / m) * 0.5 + 0.5; + for (let j = 0; j < 15; ++j) { + c[j * 3 + 0] = Math.floor((c[j * 3 + 0] / max * 0.5 + 0.5) * t11 + 0.5); + c[j * 3 + 1] = Math.floor((c[j * 3 + 1] / max * 0.5 + 0.5) * t10 + 0.5); + c[j * 3 + 2] = Math.floor((c[j * 3 + 2] / max * 0.5 + 0.5) * t11 + 0.5); } // pack - float32[0] = m; + float32[0] = max; sh1to3Data[i * 4 + 0] = uint32[0]; - sh1to3Data[i * 4 + 1] = pack(c[0], c[15], c[30]); - sh1to3Data[i * 4 + 2] = pack(c[1], c[16], c[31]); - sh1to3Data[i * 4 + 3] = pack(c[2], c[17], c[32]); - - sh4to7Data[i * 4 + 0] = pack(c[3], c[18], c[33]); - sh4to7Data[i * 4 + 1] = pack(c[4], c[19], c[34]); - sh4to7Data[i * 4 + 2] = pack(c[5], c[20], c[35]); - sh4to7Data[i * 4 + 3] = pack(c[6], c[21], c[36]); - - sh8to11Data[i * 4 + 0] = pack(c[7], c[22], c[37]); - sh8to11Data[i * 4 + 1] = pack(c[8], c[23], c[38]); - sh8to11Data[i * 4 + 2] = pack(c[9], c[24], c[39]); - sh8to11Data[i * 4 + 3] = pack(c[10], c[25], c[40]); - - sh12to15Data[i * 4 + 0] = pack(c[11], c[26], c[41]); - sh12to15Data[i * 4 + 1] = pack(c[12], c[27], c[42]); - sh12to15Data[i * 4 + 2] = pack(c[13], c[28], c[43]); - sh12to15Data[i * 4 + 3] = pack(c[14], c[29], c[44]); + sh1to3Data[i * 4 + 1] = c[0] << 21 | c[1] << 11 | c[2]; + sh1to3Data[i * 4 + 2] = c[3] << 21 | c[4] << 11 | c[5]; + sh1to3Data[i * 4 + 3] = c[6] << 21 | c[7] << 11 | c[8]; + + sh4to7Data[i * 4 + 0] = c[9] << 21 | c[10] << 11 | c[11]; + sh4to7Data[i * 4 + 1] = c[12] << 21 | c[13] << 11 | c[14]; + sh4to7Data[i * 4 + 2] = c[15] << 21 | c[16] << 11 | c[17]; + sh4to7Data[i * 4 + 3] = c[18] << 21 | c[19] << 11 | c[20]; + + sh8to11Data[i * 4 + 0] = c[21] << 21 | c[22] << 11 | c[23]; + sh8to11Data[i * 4 + 1] = c[24] << 21 | c[25] << 11 | c[26]; + sh8to11Data[i * 4 + 2] = c[27] << 21 | c[28] << 11 | c[29]; + sh8to11Data[i * 4 + 3] = c[30] << 21 | c[31] << 11 | c[32]; + + sh12to15Data[i * 4 + 0] = c[33] << 21 | c[34] << 11 | c[35]; + sh12to15Data[i * 4 + 1] = c[36] << 21 | c[37] << 11 | c[38]; + sh12to15Data[i * 4 + 2] = c[39] << 21 | c[40] << 11 | c[41]; + sh12to15Data[i * 4 + 3] = c[42] << 21 | c[43] << 11 | c[44]; } this.sh1to3Texture.unlock(); diff --git a/src/scene/gsplat/shader-generator-gsplat.js b/src/scene/gsplat/shader-generator-gsplat.js deleted file mode 100644 index ff3d0d0144a..00000000000 --- a/src/scene/gsplat/shader-generator-gsplat.js +++ /dev/null @@ -1,306 +0,0 @@ -import { hashCode } from '../../core/hash.js'; -import { SEMANTIC_ATTR13, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; -import { ShaderUtils } from '../../platform/graphics/shader-utils.js'; -import { DITHER_NONE, TONEMAP_LINEAR } from '../constants.js'; -import { shaderChunks } from '../shader-lib/chunks/chunks.js'; -import { ShaderGenerator } from '../shader-lib/programs/shader-generator.js'; -import { ShaderPass } from '../shader-pass.js'; - -const splatCoreVS = /* glsl */ ` - uniform mat4 matrix_model; - uniform mat4 matrix_view; - uniform mat4 matrix_projection; - - uniform vec2 viewport; - uniform vec4 tex_params; // num splats, texture width - - uniform highp usampler2D splatOrder; - uniform highp usampler2D transformA; - uniform highp sampler2D transformB; - - attribute vec3 vertex_position; - attribute uint vertex_id_attrib; - - #ifndef DITHER_NONE - varying float id; - #endif - - uint orderId; - uint splatId; - ivec2 splatUV; - - // calculate the current splat index and uv - bool calcSplatUV() { - uint numSplats = uint(tex_params.x); - uint textureWidth = uint(tex_params.y); - - // calculate splat index - orderId = vertex_id_attrib + uint(vertex_position.z); - - if (orderId >= numSplats) { - return false; - } - - ivec2 orderUV = ivec2( - int(orderId % textureWidth), - int(orderId / textureWidth) - ); - - // calculate splatUV - splatId = texelFetch(splatOrder, orderUV, 0).r; - splatUV = ivec2( - int(splatId % textureWidth), - int(splatId / textureWidth) - ); - - return true; - } - - uvec4 tA; - - vec3 getCenter() { - tA = texelFetch(transformA, splatUV, 0); - return uintBitsToFloat(tA.xyz); - } - - void getCovariance(out vec3 covA, out vec3 covB) { - vec4 tB = texelFetch(transformB, splatUV, 0); - vec2 tC = unpackHalf2x16(tA.w); - - covA = tB.xyz; - covB = vec3(tC.x, tC.y, tB.w); - } - - // calculate 2d covariance vectors - vec4 calcV1V2(in vec3 splat_cam, in vec3 covA, in vec3 covB, mat3 W) { - mat3 Vrk = mat3( - covA.x, covA.y, covA.z, - covA.y, covB.x, covB.y, - covA.z, covB.y, covB.z - ); - - float focal = viewport.x * matrix_projection[0][0]; - - float J1 = focal / splat_cam.z; - vec2 J2 = -J1 / splat_cam.z * splat_cam.xy; - mat3 J = mat3( - J1, 0.0, J2.x, - 0.0, J1, J2.y, - 0.0, 0.0, 0.0 - ); - - mat3 T = W * J; - mat3 cov = transpose(T) * Vrk * T; - - float diagonal1 = cov[0][0] + 0.3; - float offDiagonal = cov[0][1]; - float diagonal2 = cov[1][1] + 0.3; - - float mid = 0.5 * (diagonal1 + diagonal2); - float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); - float lambda1 = mid + radius; - float lambda2 = max(mid - radius, 0.1); - vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); - - vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector; - vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x); - - return vec4(v1, v2); - } - - - // Spherical Harmonics - - vec3 unpack111011(uint bits) { - return vec3( - float(bits >> 21u) / 2047.0, - float((bits >> 11u) & 0x3ffu) / 1023.0, - float(bits & 0x7ffu) / 2047.0 - ); - } - - // fetch quantized spherical harmonic coefficients - void fetchScale(in uvec4 t, out float scale, out vec3 a, out vec3 b, out vec3 c) { - scale = uintBitsToFloat(t.x); - a = unpack111011(t.y) * 2.0 - 1.0; - b = unpack111011(t.z) * 2.0 - 1.0; - c = unpack111011(t.w) * 2.0 - 1.0; - } - - // fetch quantized spherical harmonic coefficients - void fetch(in uvec4 t, out vec3 a, out vec3 b, out vec3 c, out vec3 d) { - a = unpack111011(t.x) * 2.0 - 1.0; - b = unpack111011(t.y) * 2.0 - 1.0; - c = unpack111011(t.z) * 2.0 - 1.0; - d = unpack111011(t.w) * 2.0 - 1.0; - } - - #if defined(USE_SH1) - #define SH_C1 0.4886025119029199f - - uniform highp usampler2D splatSH_1to3; - #if defined(USE_SH2) - #define SH_C2_0 1.0925484305920792f - #define SH_C2_1 -1.0925484305920792f - #define SH_C2_2 0.31539156525252005f - #define SH_C2_3 -1.0925484305920792f - #define SH_C2_4 0.5462742152960396f - - uniform highp usampler2D splatSH_4to7; - uniform highp usampler2D splatSH_8to11; - #if defined(USE_SH3) - #define SH_C3_0 -0.5900435899266435f - #define SH_C3_1 2.890611442640554f - #define SH_C3_2 -0.4570457994644658f - #define SH_C3_3 0.3731763325901154f - #define SH_C3_4 -0.4570457994644658f - #define SH_C3_5 1.445305721320277f - #define SH_C3_6 -0.5900435899266435f - - uniform highp usampler2D splatSH_12to15; - #endif - #endif - #endif - - vec3 evalSH(in vec3 dir) { - vec3 result = vec3(0.0); - - // see https://github.com/graphdeco-inria/gaussian-splatting/blob/main/utils/sh_utils.py - #if defined(USE_SH1) - // 1st degree - float x = dir.x; - float y = dir.y; - float z = dir.z; - - float scale; - vec3 sh1, sh2, sh3; - fetchScale(texelFetch(splatSH_1to3, splatUV, 0), scale, sh1, sh2, sh3); - result += SH_C1 * (-sh1 * y + sh2 * z - sh3 * x); - - #if defined(USE_SH2) - // 2nd degree - float xx = x * x; - float yy = y * y; - float zz = z * z; - float xy = x * y; - float yz = y * z; - float xz = x * z; - - vec3 sh4, sh5, sh6, sh7; - vec3 sh8, sh9, sh10, sh11; - fetch(texelFetch(splatSH_4to7, splatUV, 0), sh4, sh5, sh6, sh7); - fetch(texelFetch(splatSH_8to11, splatUV, 0), sh8, sh9, sh10, sh11); - result += - sh4 * (SH_C2_0 * xy) * + - sh5 * (SH_C2_1 * yz) + - sh6 * (SH_C2_2 * (2.0 * zz - xx - yy)) + - sh7 * (SH_C2_3 * xz) + - sh8 * (SH_C2_4 * (xx - yy)); - - #if defined(USE_SH3) - // 3rd degree - vec3 sh12, sh13, sh14, sh15; - fetch(texelFetch(splatSH_12to15, splatUV, 0), sh12, sh13, sh14, sh15); - result += - sh9 * (SH_C3_0 * y * (3.0 * xx - yy)) + - sh10 * (SH_C3_1 * xy * z) + - sh11 * (SH_C3_2 * y * (4.0 * zz - xx - yy)) + - sh12 * (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) + - sh13 * (SH_C3_4 * x * (4.0 * zz - xx - yy)) + - sh14 * (SH_C3_5 * z * (xx - yy)) + - sh15 * (SH_C3_6 * x * (xx - 3.0 * yy)); - #endif - #endif - result *= scale; - #endif - - return result; - } -`; - -const splatCoreFS = /* glsl */ ` - #ifndef DITHER_NONE - varying float id; - #endif - - #ifdef PICK_PASS - uniform vec4 uColor; - #endif - - vec4 evalSplat(vec2 texCoord, vec4 color) { - mediump float A = dot(texCoord, texCoord); - if (A > 1.0) { - discard; - } - - mediump float B = exp(-A * 4.0) * color.a; - if (B < 1.0 / 255.0) { - discard; - } - - #ifdef PICK_PASS - if (B < 0.3) { - discard; - } - return uColor; - #endif - - #ifndef DITHER_NONE - opacityDither(B, id * 0.013); - #endif - - #ifdef TONEMAP_ENABLED - return vec4(gammaCorrectOutput(toneMap(decodeGamma(color.rgb))), B); - #else - return vec4(color.rgb, B); - #endif - } -`; - -class GSplatShaderGenerator { - generateKey(options) { - const vsHash = hashCode(options.vertex); - const fsHash = hashCode(options.fragment); - const definesHash = ShaderGenerator.definesHash(options.defines); - return `splat-${options.pass}-${options.gamma}-${options.toneMapping}-${vsHash}-${fsHash}-${options.dither}-${definesHash}`; - } - - createShaderDefinition(device, options) { - - const shaderPassInfo = ShaderPass.get(device).getByIndex(options.pass); - const shaderPassDefines = shaderPassInfo.shaderDefines; - - const defines = - `${shaderPassDefines}\n` + - `#define DITHER_${options.dither.toUpperCase()}\n` + - `#define TONEMAP_${options.toneMapping === TONEMAP_LINEAR ? 'DISABLED' : 'ENABLED'}\n`; - - const vs = defines + splatCoreVS + options.vertex; - const fs = defines + shaderChunks.decodePS + - (options.dither === DITHER_NONE ? '' : shaderChunks.bayerPS + shaderChunks.opacityDitherPS) + - ShaderGenerator.tonemapCode(options.toneMapping) + - ShaderGenerator.gammaCode(options.gamma) + - splatCoreFS + options.fragment; - - const defineMap = new Map(); - options.defines.forEach((value, key) => { - defineMap.set(key, value); - }); - - return ShaderUtils.createDefinition(device, { - name: 'SplatShader', - attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_id_attrib: SEMANTIC_ATTR13 - }, - vertexCode: vs, - fragmentCode: fs, - fragmentDefines: defineMap, - vertexDefines: defineMap - }); - } -} - -const gsplat = new GSplatShaderGenerator(); - -export { gsplat }; diff --git a/src/scene/materials/standard-material-parameters.js b/src/scene/materials/standard-material-parameters.js index 1e3caab55a4..6079a1acb82 100644 --- a/src/scene/materials/standard-material-parameters.js +++ b/src/scene/materials/standard-material-parameters.js @@ -182,7 +182,13 @@ const standardMaterialRemovedParameters = { opacityMapVertexColor: 'boolean', specularAntialias: 'boolean', specularMapTint: 'boolean', - specularMapVertexColor: 'boolean' + specularMapVertexColor: 'boolean', + ambientTint: 'boolean', + emissiveTint: 'boolean', + diffuseTint: 'boolean', + sheenTint: 'boolean', + conserveEnergy: 'boolean', + useGamma: 'boolean' }; export { diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index d4827f4f7fd..4b8fe23f1eb 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -225,8 +225,8 @@ class MeshInstance { visible = true; /** - * Read this value in {@link CameraComponent#onPostCull} to determine if the object is actually going to - * be rendered. + * Read this value in {@link Scene#EVENT_POSTCULL} event to determine if the object is actually + * going to be rendered. * * @type {boolean} */ diff --git a/src/scene/renderer/render-pass-forward.js b/src/scene/renderer/render-pass-forward.js index 50d41d06b2a..407d5778ac7 100644 --- a/src/scene/renderer/render-pass-forward.js +++ b/src/scene/renderer/render-pass-forward.js @@ -6,7 +6,7 @@ import { BlendState } from '../../platform/graphics/blend-state.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { RenderPass } from '../../platform/graphics/render-pass.js'; import { RenderAction } from '../composition/render-action.js'; -import { SHADER_FORWARD } from '../constants.js'; +import { EVENT_POSTRENDER, EVENT_POSTRENDER_LAYER, EVENT_PRERENDER, EVENT_PRERENDER_LAYER, SHADER_FORWARD } from '../constants.js'; /** * @import { CameraComponent } from '../../framework/components/camera/component.js' @@ -201,11 +201,11 @@ class RenderPassForward extends RenderPass { before() { const { renderActions } = this; - // onPreRender callbacks + // onPreRender events for (let i = 0; i < renderActions.length; i++) { const ra = renderActions[i]; if (ra.firstCameraUse) { - ra.camera.onPreRender?.(); + this.scene.fire(EVENT_PRERENDER, ra.camera); } } } @@ -231,11 +231,11 @@ class RenderPassForward extends RenderPass { after() { - // onPostRender callbacks + // onPostRender events for (let i = 0; i < this.renderActions.length; i++) { const ra = this.renderActions[i]; if (ra.lastCameraUse) { - ra.camera.onPostRender?.(); + this.scene.fire(EVENT_POSTRENDER, ra.camera); } } @@ -249,7 +249,7 @@ class RenderPassForward extends RenderPass { */ renderRenderAction(renderAction, firstRenderAction) { - const { renderer } = this; + const { renderer, scene } = this; const device = renderer.device; // layer @@ -263,8 +263,8 @@ class RenderPassForward extends RenderPass { if (camera) { - // layer pre render callback - camera.onPreRenderLayer?.(layer, transparent); + // layer pre render event + scene.fire(EVENT_PRERENDER_LAYER, camera, layer, transparent); const options = { lightClusters: renderAction.lightClusters @@ -292,8 +292,8 @@ class RenderPassForward extends RenderPass { device.setStencilState(null, null); device.setAlphaToCoverage(false); - // layer post render callback - camera.onPostRenderLayer?.(layer, transparent); + // layer post render event + scene.fire(EVENT_POSTRENDER_LAYER, camera, layer, transparent); } DebugGraphics.popGpuMarker(this.device); diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index d6debefa5fe..945f9a44234 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -25,7 +25,9 @@ import { SORTKEY_DEPTH, SORTKEY_FORWARD, VIEW_CENTER, PROJECTION_ORTHOGRAPHIC, LIGHTTYPE_DIRECTIONAL, MASK_AFFECT_DYNAMIC, MASK_AFFECT_LIGHTMAPPED, MASK_BAKE, - SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME + SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, + EVENT_PRECULL, + EVENT_POSTCULL } from '../constants.js'; import { LightCube } from '../graphics/light-cube.js'; import { getBlueNoiseTexture } from '../graphics/noise-textures.js'; @@ -1129,6 +1131,8 @@ class Renderer { const cullTime = now(); // #endif + const { scene } = this; + this.processingMeshInstances.clear(); // for all cameras @@ -1138,8 +1142,8 @@ class Renderer { for (let i = 0; i < numCameras; i++) { const camera = comp.cameras[i]; - // callback before the camera is culling - camera.onPreCull?.(); + // event before the camera is culling + scene?.fire(EVENT_PRECULL, camera); // update camera and frustum const renderTarget = camera.renderTarget; @@ -1162,13 +1166,13 @@ class Renderer { } } - // callback after the camera is done with culling - camera.onPostCull?.(); + // event after the camera is done with culling + scene?.fire(EVENT_POSTCULL, camera); } // update shadow / cookie atlas allocation for the visible lights. Update it after the ligthts were culled, // but before shadow maps were culling, as it might force some 'update once' shadows to cull. - if (this.scene.clusteredLightingEnabled) { + if (scene.clusteredLightingEnabled) { this.updateLightTextureAtlas(); } diff --git a/src/scene/scene.js b/src/scene/scene.js index a37a3153c5c..9554c2f8049 100644 --- a/src/scene/scene.js +++ b/src/scene/scene.js @@ -67,6 +67,80 @@ class Scene extends EventHandler { */ static EVENT_SETSKYBOX = 'set:skybox'; + /** + * Fired before the camera renders the scene. The handler is passed the {@link CameraComponent} + * that will render the scene. + * + * @event + * @example + * app.scene.on('prerender', (camera) => { + * console.log(`Camera ${camera.entity.name} will render the scene`); + * }); + */ + static EVENT_PRERENDER = 'prerender'; + + /** + * Fired when the camera renders the scene. The handler is passed the {@link CameraComponent} + * that rendered the scene. + * + * @event + * @example + * app.scene.on('postrender', (camera) => { + * console.log(`Camera ${camera.entity.name} rendered the scene`); + * }); + */ + static EVENT_POSTRENDER = 'postrender'; + + /** + * Fired before the camera renders a layer. The handler is passed the {@link CameraComponent}, + * the {@link Layer} that will be rendered, and a boolean parameter set to true if the layer is + * transparent. This is called during rendering to a render target or a default framebuffer, and + * additional rendering can be performed here, for example using {@link QuadRender#render}. + * + * @event + * @example + * app.scene.on('prerender:layer', (camera, layer, transparent) => { + * console.log(`Camera ${camera.entity.name} will render the layer ${layer.name} (transparent: ${transparent})`); + * }); + */ + static EVENT_PRERENDER_LAYER = 'prerender:layer'; + + /** + * Fired when the camera renders a layer. The handler is passed the {@link CameraComponent}, + * the {@link Layer} that will be rendered, and a boolean parameter set to true if the layer is + * transparent. This is called during rendering to a render target or a default framebuffer, and + * additional rendering can be performed here, for example using {@link QuadRender#render}. + * + * @event + * @example + * app.scene.on('postrender:layer', (camera, layer, transparent) => { + * console.log(`Camera ${camera.entity.name} rendered the layer ${layer.name} (transparent: ${transparent})`); + * }); + */ + static EVENT_POSTRENDER_LAYER = 'postrender:layer'; + + /** + * Fired before visibility culling is performed for the camera. + * + * @event + * @example + * app.scene.on('precull', (camera) => { + * console.log(`Visibility culling will be performed for camera ${camera.entity.name}`); + * }); + */ + static EVENT_PRECULL = 'precull'; + + /** + * Fired after visibility culling is performed for the camera. + * + * @event + * @example + * app.scene.on('postcull', (camera) => { + * console.log(`Visibility culling was performed for camera ${camera.entity.name}`); + * }); + */ + static EVENT_POSTCULL = 'postcull'; + /** * If enabled, the ambient lighting will be baked into lightmaps. This will be either the * {@link Scene#skybox} if set up, otherwise {@link Scene#ambientLight}. Defaults to false. diff --git a/src/scene/shader-lib/chunks/chunks.js b/src/scene/shader-lib/chunks/chunks.js index 05d440c8c14..3c5756634dd 100644 --- a/src/scene/shader-lib/chunks/chunks.js +++ b/src/scene/shader-lib/chunks/chunks.js @@ -56,6 +56,18 @@ import gamma2_2PS from './common/frag/gamma2_2.js'; import gles3PS from '../../../platform/graphics/shader-chunks/frag/gles3.js'; import gles3VS from '../../../platform/graphics/shader-chunks/vert/gles3.js'; import glossPS from './standard/frag/gloss.js'; +import gsplatCenterVS from './gsplat/vert/gsplatCenter.js'; +import gsplatColorVS from './gsplat/vert/gsplatColor.js'; +import gsplatCommonVS from './gsplat/vert/gsplatCommon.js'; +import gsplatCompressedDataVS from './gsplat/vert/gsplatCompressedData.js'; +import gsplatCompressedSHVS from './gsplat/vert/gsplatCompressedSH.js'; +import gsplatCornerVS from './gsplat/vert/gsplatCorner.js'; +import gsplatDataVS from './gsplat/vert/gsplatData.js'; +import gsplatOutputVS from './gsplat/vert/gsplatOutput.js'; +import gsplatPS from './gsplat/frag/gsplat.js'; +import gsplatSHVS from './gsplat/vert/gsplatSH.js'; +import gsplatSourceVS from './gsplat/vert/gsplatSource.js'; +import gsplatVS from './gsplat/vert/gsplat.js'; import iridescenceDiffractionPS from './lit/frag/iridescenceDiffraction.js'; import iridescencePS from './standard/frag/iridescence.js'; import iridescenceThicknessPS from './standard/frag/iridescenceThickness.js'; @@ -172,13 +184,14 @@ import TBNPS from './lit/frag/TBN.js'; import TBNderivativePS from './lit/frag/TBNderivative.js'; import TBNObjectSpacePS from './lit/frag/TBNObjectSpace.js'; import thicknessPS from './standard/frag/thickness.js'; -import tonemappingAcesPS from './common/frag/tonemappingAces.js'; -import tonemappingAces2PS from './common/frag/tonemappingAces2.js'; -import tonemappingFilmicPS from './common/frag/tonemappingFilmic.js'; -import tonemappingHejlPS from './common/frag/tonemappingHejl.js'; -import tonemappingLinearPS from './common/frag/tonemappingLinear.js'; -import tonemappingNeutralPS from './common/frag/tonemappingNeutral.js'; -import tonemappingNonePS from './common/frag/tonemappingNone.js'; +import tonemappingPS from './common/frag/tonemapping/tonemapping.js'; +import tonemappingAcesPS from './common/frag/tonemapping/tonemappingAces.js'; +import tonemappingAces2PS from './common/frag/tonemapping/tonemappingAces2.js'; +import tonemappingFilmicPS from './common/frag/tonemapping/tonemappingFilmic.js'; +import tonemappingHejlPS from './common/frag/tonemapping/tonemappingHejl.js'; +import tonemappingLinearPS from './common/frag/tonemapping/tonemappingLinear.js'; +import tonemappingNeutralPS from './common/frag/tonemapping/tonemappingNeutral.js'; +import tonemappingNonePS from './common/frag/tonemapping/tonemappingNone.js'; import transformVS from './common/vert/transform.js'; import transformCoreVS from './common/vert/transformCore.js'; import transformInstancingVS from './common/vert/transformInstancing.js'; @@ -256,6 +269,18 @@ const shaderChunks = { gles3PS, gles3VS, glossPS, + gsplatCenterVS, + gsplatCornerVS, + gsplatColorVS, + gsplatCommonVS, + gsplatCompressedDataVS, + gsplatCompressedSHVS, + gsplatDataVS, + gsplatOutputVS, + gsplatPS, + gsplatSHVS, + gsplatSourceVS, + gsplatVS, iridescenceDiffractionPS, iridescencePS, iridescenceThicknessPS, @@ -372,6 +397,7 @@ const shaderChunks = { TBNderivativePS, TBNObjectSpacePS, thicknessPS, + tonemappingPS, tonemappingAcesPS, tonemappingAces2PS, tonemappingFilmicPS, diff --git a/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemapping.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemapping.js new file mode 100644 index 00000000000..7bb0e6b4c02 --- /dev/null +++ b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemapping.js @@ -0,0 +1,19 @@ +export default /* glsl */` + +#if (TONEMAP == NONE) + #include "tonemappingNonePS" +#elif TONEMAP == FILMIC + #include "tonemappingFilmicPS" +#elif TONEMAP == LINEAR + #include "tonemappingLinearPS" +#elif TONEMAP == HEJL + #include "tonemappingHejlPS" +#elif TONEMAP == ACES + #include "tonemappingAcesPS" +#elif TONEMAP == ACES2 + #include "tonemappingAces2PS" +#elif TONEMAP == NEUTRAL + #include "tonemappingNeutralPS" +#endif + +`; diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingAces.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingAces.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingAces.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingAces.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingAces2.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingAces2.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingAces2.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingAces2.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingFilmic.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingFilmic.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingFilmic.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingFilmic.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingHejl.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingHejl.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingHejl.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingHejl.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingLinear.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingLinear.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingLinear.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingLinear.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingNeutral.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingNeutral.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingNeutral.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingNeutral.js diff --git a/src/scene/shader-lib/chunks/common/frag/tonemappingNone.js b/src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingNone.js similarity index 100% rename from src/scene/shader-lib/chunks/common/frag/tonemappingNone.js rename to src/scene/shader-lib/chunks/common/frag/tonemapping/tonemappingNone.js diff --git a/src/scene/shader-lib/chunks/gsplat/frag/gsplat.js b/src/scene/shader-lib/chunks/gsplat/frag/gsplat.js new file mode 100644 index 00000000000..e83197c20b0 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/frag/gsplat.js @@ -0,0 +1,42 @@ +export default /* glsl */` + +#ifndef DITHER_NONE + #include "bayerPS" + #include "opacityDitherPS" + varying float id; +#endif + +#ifdef PICK_PASS + uniform vec4 uColor; +#endif + +varying mediump vec2 gaussianUV; +varying mediump vec4 gaussianColor; + +void main(void) { + mediump float A = dot(gaussianUV, gaussianUV); + if (A > 1.0) { + discard; + } + + // evaluate alpha + mediump float alpha = exp(-A * 4.0) * gaussianColor.a; + + #ifdef PICK_PASS + if (alpha < 0.3) { + discard; + } + gl_FragColor = uColor; + #else + if (alpha < 1.0 / 255.0) { + discard; + } + + #ifndef DITHER_NONE + opacityDither(alpha, id * 0.013); + #endif + + gl_FragColor = vec4(gaussianColor.xyz, alpha); + #endif +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplat.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplat.js new file mode 100644 index 00000000000..02e20293135 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplat.js @@ -0,0 +1,54 @@ +export default /* glsl */` +#include "gsplatCommonVS" + +varying mediump vec2 gaussianUV; +varying mediump vec4 gaussianColor; + +#ifndef DITHER_NONE + varying float id; +#endif + +mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); + +void main(void) { + // read gaussian details + SplatSource source; + if (!initSource(source)) { + gl_Position = discardVec; + return; + } + + vec3 modelCenter = readCenter(source); + + SplatCenter center; + initCenter(source, modelCenter, center); + + // project center to screen space + SplatCorner corner; + if (!initCorner(source, center, corner)) { + gl_Position = discardVec; + return; + } + + // read color + vec4 clr = readColor(source); + + // evaluate spherical harmonics + #if SH_BANDS > 0 + // calculate the model-space view direction + vec3 dir = normalize(center.view * mat3(center.modelView)); + clr.xyz += evalSH(source, dir); + #endif + + clipCorner(corner, clr.w); + + // write output + gl_Position = center.proj + vec4(corner.offset, 0, 0); + gaussianUV = corner.uv; + gaussianColor = vec4(prepareOutputFromGamma(max(clr.xyz, 0.0)), clr.w); + + #ifndef DITHER_NONE + id = float(source.id); + #endif +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatCenter.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCenter.js new file mode 100644 index 00000000000..bf20c944e7d --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCenter.js @@ -0,0 +1,27 @@ +export default /* glsl */` +uniform mat4 matrix_model; +uniform mat4 matrix_view; +uniform mat4 matrix_projection; + +// project the model space gaussian center to view and clip space +bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) { + mat4 modelView = matrix_view * matrix_model; + vec4 centerView = modelView * vec4(modelCenter, 1.0); + + // early out if splat is behind the camear + if (centerView.z > 0.0) { + return false; + } + + vec4 centerProj = matrix_projection * centerView; + + // ensure gaussians are not clipped by camera near and far + centerProj.z = clamp(centerProj.z, -abs(centerProj.w), abs(centerProj.w)); + + center.view = centerView.xyz / centerView.w; + center.proj = centerProj; + center.projMat00 = matrix_projection[0][0]; + center.modelView = modelView; + return true; +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatColor.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatColor.js new file mode 100644 index 00000000000..869b3016c19 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatColor.js @@ -0,0 +1,9 @@ +export default /* glsl */` + +uniform mediump sampler2D splatColor; + +vec4 readColor(in SplatSource source) { + return texelFetch(splatColor, source.uv, 0); +} + +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatCommon.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCommon.js new file mode 100644 index 00000000000..a6c13bb1f0a --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCommon.js @@ -0,0 +1,128 @@ +export default /* glsl */` + +// stores the source UV and order of the splat +struct SplatSource { + uint order; // render order + uint id; // splat id + ivec2 uv; // splat uv + vec2 cornerUV; // corner coordinates for this vertex of the gaussian (-1, -1)..(1, 1) +}; + +// stores the camera and clip space position of the gaussian center +struct SplatCenter { + vec3 view; // center in view space + vec4 proj; // center in clip space + mat4 modelView; // model-view matrix + float projMat00; // elememt [0][0] of the projection matrix +}; + +// stores the offset from center for the current gaussian +struct SplatCorner { + vec2 offset; // corner offset from center in clip space + vec2 uv; // corner uv +}; + +#if SH_BANDS > 0 + #if SH_BANDS == 1 + #define SH_COEFFS 3 + #elif SH_BANDS == 2 + #define SH_COEFFS 8 + #elif SH_BANDS == 3 + #define SH_COEFFS 15 + #endif +#endif + +#if GSPLAT_COMPRESSED_DATA == true + #include "gsplatCompressedDataVS" + #include "gsplatCompressedSHVS" +#else + #include "gsplatDataVS" + #include "gsplatColorVS" + #include "gsplatSHVS" +#endif + +#include "gsplatSourceVS" +#include "gsplatCenterVS" +#include "gsplatCornerVS" +#include "gsplatOutputVS" + +// modify the gaussian corner so it excludes gaussian regions with alpha +// less than 1/255 +void clipCorner(inout SplatCorner corner, float alpha) { + float clip = min(1.0, sqrt(-log(1.0 / 255.0 / alpha)) / 2.0); + corner.offset *= clip; + corner.uv *= clip; +} + +// spherical Harmonics + +#if SH_BANDS > 0 + +#define SH_C1 0.4886025119029199f + +#if SH_BANDS > 1 + #define SH_C2_0 1.0925484305920792f + #define SH_C2_1 -1.0925484305920792f + #define SH_C2_2 0.31539156525252005f + #define SH_C2_3 -1.0925484305920792f + #define SH_C2_4 0.5462742152960396f +#endif + +#if SH_BANDS > 2 + #define SH_C3_0 -0.5900435899266435f + #define SH_C3_1 2.890611442640554f + #define SH_C3_2 -0.4570457994644658f + #define SH_C3_3 0.3731763325901154f + #define SH_C3_4 -0.4570457994644658f + #define SH_C3_5 1.445305721320277f + #define SH_C3_6 -0.5900435899266435f +#endif + +// see https://github.com/graphdeco-inria/gaussian-splatting/blob/main/utils/sh_utils.py +vec3 evalSH(in SplatSource source, in vec3 dir) { + + // read sh coefficients + vec3 sh[SH_COEFFS]; + float scale; + readSHData(source, sh, scale); + + float x = dir.x; + float y = dir.y; + float z = dir.z; + + // 1st degree + vec3 result = SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x); + +#if SH_BANDS > 1 + // 2nd degree + float xx = x * x; + float yy = y * y; + float zz = z * z; + float xy = x * y; + float yz = y * z; + float xz = x * z; + + result += + sh[3] * (SH_C2_0 * xy) * + + sh[4] * (SH_C2_1 * yz) + + sh[5] * (SH_C2_2 * (2.0 * zz - xx - yy)) + + sh[6] * (SH_C2_3 * xz) + + sh[7] * (SH_C2_4 * (xx - yy)); +#endif + +#if SH_BANDS > 2 + // 3rd degree + result += + sh[8] * (SH_C3_0 * y * (3.0 * xx - yy)) + + sh[9] * (SH_C3_1 * xy * z) + + sh[10] * (SH_C3_2 * y * (4.0 * zz - xx - yy)) + + sh[11] * (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) + + sh[12] * (SH_C3_4 * x * (4.0 * zz - xx - yy)) + + sh[13] * (SH_C3_5 * z * (xx - yy)) + + sh[14] * (SH_C3_6 * x * (xx - 3.0 * yy)); +#endif + + return result * scale; +} +#endif +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedData.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedData.js new file mode 100644 index 00000000000..e3149bf5269 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedData.js @@ -0,0 +1,108 @@ +export default /* glsl */` +uniform highp usampler2D packedTexture; +uniform highp sampler2D chunkTexture; + +// work values +vec4 chunkDataA; // x: min_x, y: min_y, z: min_z, w: max_x +vec4 chunkDataB; // x: max_y, y: max_z, z: scale_min_x, w: scale_min_y +vec4 chunkDataC; // x: scale_min_z, y: scale_max_x, z: scale_max_y, w: scale_max_z +vec4 chunkDataD; // x: min_r, y: min_g, z: min_b, w: max_r +vec4 chunkDataE; // x: max_g, y: max_b, z: unused, w: unused +uvec4 packedData; // x: position bits, y: rotation bits, z: scale bits, w: color bits + +vec3 unpack111011(uint bits) { + return vec3( + float(bits >> 21u) / 2047.0, + float((bits >> 11u) & 0x3ffu) / 1023.0, + float(bits & 0x7ffu) / 2047.0 + ); +} + +vec4 unpack8888(uint bits) { + return vec4( + float(bits >> 24u) / 255.0, + float((bits >> 16u) & 0xffu) / 255.0, + float((bits >> 8u) & 0xffu) / 255.0, + float(bits & 0xffu) / 255.0 + ); +} + +const float norm = 1.0 / (sqrt(2.0) * 0.5); + +vec4 unpackRotation(uint bits) { + float a = (float((bits >> 20u) & 0x3ffu) / 1023.0 - 0.5) * norm; + float b = (float((bits >> 10u) & 0x3ffu) / 1023.0 - 0.5) * norm; + float c = (float(bits & 0x3ffu) / 1023.0 - 0.5) * norm; + float m = sqrt(1.0 - (a * a + b * b + c * c)); + + uint mode = bits >> 30u; + if (mode == 0u) return vec4(m, a, b, c); + if (mode == 1u) return vec4(a, m, b, c); + if (mode == 2u) return vec4(a, b, m, c); + return vec4(a, b, c, m); +} + +mat3 quatToMat3(vec4 R) { + float x = R.x; + float y = R.y; + float z = R.z; + float w = R.w; + return mat3( + 1.0 - 2.0 * (z * z + w * w), + 2.0 * (y * z + x * w), + 2.0 * (y * w - x * z), + 2.0 * (y * z - x * w), + 1.0 - 2.0 * (y * y + w * w), + 2.0 * (z * w + x * y), + 2.0 * (y * w + x * z), + 2.0 * (z * w - x * y), + 1.0 - 2.0 * (y * y + z * z) + ); +} + +// read center +vec3 readCenter(SplatSource source) { + uint w = uint(textureSize(chunkTexture, 0).x) / 5; + uint chunkId = source.id / 256u; + ivec2 chunkUV = ivec2((chunkId % w) * 5u, chunkId / w); + + // read chunk and packed compressed data + chunkDataA = texelFetch(chunkTexture, chunkUV, 0); + chunkDataB = texelFetch(chunkTexture, chunkUV + ivec2(1, 0), 0); + chunkDataC = texelFetch(chunkTexture, chunkUV + ivec2(2, 0), 0); + chunkDataD = texelFetch(chunkTexture, chunkUV + ivec2(3, 0), 0); + chunkDataE = texelFetch(chunkTexture, chunkUV + ivec2(4, 0), 0); + packedData = texelFetch(packedTexture, source.uv, 0); + + return mix(chunkDataA.xyz, vec3(chunkDataA.w, chunkDataB.xy), unpack111011(packedData.x)); +} + +vec4 readColor(in SplatSource source) { + vec4 r = unpack8888(packedData.w); + return vec4(mix(chunkDataD.xyz, vec3(chunkDataD.w, chunkDataE.xy), r.rgb), r.w); +} + +vec4 getRotation() { + return unpackRotation(packedData.y); +} + +vec3 getScale() { + return exp(mix(vec3(chunkDataB.zw, chunkDataC.x), chunkDataC.yzw, unpack111011(packedData.z))); +} + +// given a rotation matrix and scale vector, compute 3d covariance A and B +void readCovariance(in SplatSource source, out vec3 covA, out vec3 covB) { + mat3 rot = quatToMat3(getRotation()); + vec3 scale = getScale(); + + // M = S * R + mat3 M = transpose(mat3( + scale.x * rot[0], + scale.y * rot[1], + scale.z * rot[2] + )); + + covA = vec3(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2])); + covB = vec3(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2])); +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedSH.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedSH.js new file mode 100644 index 00000000000..00960780376 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCompressedSH.js @@ -0,0 +1,53 @@ +export default /* glsl */` +#if SH_BANDS > 0 + +uniform highp usampler2D shTexture0; +uniform highp usampler2D shTexture1; +uniform highp usampler2D shTexture2; + +vec4 unpack8888s(in uint bits) { + return vec4((uvec4(bits) >> uvec4(0u, 8u, 16u, 24u)) & 0xffu) * (8.0 / 255.0) - 4.0; +} + +void readSHData(in SplatSource source, out vec3 sh[15], out float scale) { + // read the sh coefficients + uvec4 shData0 = texelFetch(shTexture0, source.uv, 0); + uvec4 shData1 = texelFetch(shTexture1, source.uv, 0); + uvec4 shData2 = texelFetch(shTexture2, source.uv, 0); + + vec4 r0 = unpack8888s(shData0.x); + vec4 r1 = unpack8888s(shData0.y); + vec4 r2 = unpack8888s(shData0.z); + vec4 r3 = unpack8888s(shData0.w); + + vec4 g0 = unpack8888s(shData1.x); + vec4 g1 = unpack8888s(shData1.y); + vec4 g2 = unpack8888s(shData1.z); + vec4 g3 = unpack8888s(shData1.w); + + vec4 b0 = unpack8888s(shData2.x); + vec4 b1 = unpack8888s(shData2.y); + vec4 b2 = unpack8888s(shData2.z); + vec4 b3 = unpack8888s(shData2.w); + + sh[0] = vec3(r0.x, g0.x, b0.x); + sh[1] = vec3(r0.y, g0.y, b0.y); + sh[2] = vec3(r0.z, g0.z, b0.z); + sh[3] = vec3(r0.w, g0.w, b0.w); + sh[4] = vec3(r1.x, g1.x, b1.x); + sh[5] = vec3(r1.y, g1.y, b1.y); + sh[6] = vec3(r1.z, g1.z, b1.z); + sh[7] = vec3(r1.w, g1.w, b1.w); + sh[8] = vec3(r2.x, g2.x, b2.x); + sh[9] = vec3(r2.y, g2.y, b2.y); + sh[10] = vec3(r2.z, g2.z, b2.z); + sh[11] = vec3(r2.w, g2.w, b2.w); + sh[12] = vec3(r3.x, g3.x, b3.x); + sh[13] = vec3(r3.y, g3.y, b3.y); + sh[14] = vec3(r3.z, g3.z, b3.z); + + scale = 1.0; +} + +#endif +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatCorner.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCorner.js new file mode 100644 index 00000000000..c2f98d64545 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatCorner.js @@ -0,0 +1,63 @@ +export default /* glsl */` +uniform vec2 viewport; // viewport dimensions +uniform vec4 camera_params; // 1 / far, far, near, isOrtho + +// calculate the clip-space offset from the center for this gaussian +bool initCorner(SplatSource source, SplatCenter center, out SplatCorner corner) { + // get covariance + vec3 covA, covB; + readCovariance(source, covA, covB); + + mat3 Vrk = mat3( + covA.x, covA.y, covA.z, + covA.y, covB.x, covB.y, + covA.z, covB.y, covB.z + ); + + float focal = viewport.x * center.projMat00; + + vec3 v = camera_params.w == 1.0 ? vec3(0.0, 0.0, 1.0) : center.view.xyz; + float J1 = focal / v.z; + vec2 J2 = -J1 / v.z * v.xy; + mat3 J = mat3( + J1, 0.0, J2.x, + 0.0, J1, J2.y, + 0.0, 0.0, 0.0 + ); + + mat3 W = transpose(mat3(center.modelView)); + mat3 T = W * J; + mat3 cov = transpose(T) * Vrk * T; + + float diagonal1 = cov[0][0] + 0.3; + float offDiagonal = cov[0][1]; + float diagonal2 = cov[1][1] + 0.3; + + float mid = 0.5 * (diagonal1 + diagonal2); + float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); + float lambda1 = mid + radius; + float lambda2 = max(mid - radius, 0.1); + + float l1 = 2.0 * min(sqrt(2.0 * lambda1), 1024.0); + float l2 = 2.0 * min(sqrt(2.0 * lambda2), 1024.0); + + // early-out gaussians smaller than 2 pixels + if (l1 < 2.0 && l2 < 2.0) { + return false; + } + + // perform cull against x/y axes + if (any(greaterThan(abs(center.proj.xy) - vec2(l1, l2) / viewport * center.proj.w, center.proj.ww))) { + return false; + } + + vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); + vec2 v1 = l1 * diagonalVector; + vec2 v2 = l2 * vec2(diagonalVector.y, -diagonalVector.x); + + corner.offset = (source.cornerUV.x * v1 + source.cornerUV.y * v2) / viewport * center.proj.w; + corner.uv = source.cornerUV; + + return true; +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatData.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatData.js new file mode 100644 index 00000000000..20ada43f768 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatData.js @@ -0,0 +1,23 @@ +export default /* glsl */` +uniform highp usampler2D transformA; +uniform highp sampler2D transformB; + +// work values +uint tAw; + +// read the model-space center of the gaussian +vec3 readCenter(SplatSource source) { + // read transform data + uvec4 tA = texelFetch(transformA, source.uv, 0); + tAw = tA.w; + return uintBitsToFloat(tA.xyz); +} + +// sample covariance vectors +void readCovariance(in SplatSource source, out vec3 covA, out vec3 covB) { + vec4 tB = texelFetch(transformB, source.uv, 0); + vec2 tC = unpackHalf2x16(tAw); + covA = tB.xyz; + covB = vec3(tC.x, tC.y, tB.w); +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatOutput.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatOutput.js new file mode 100644 index 00000000000..06d2f2808ca --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatOutput.js @@ -0,0 +1,30 @@ +export default /* glsl */` + +#include "tonemappingPS" + +#if TONEMAP != NONE + #if GAMMA == SRGB + #include "decodePS" + #include "gamma2_2PS" + #else + #include "gamma1_0PS" + #endif +#endif + +// prepare the output color for the given gamma-space color +vec3 prepareOutputFromGamma(vec3 gammaColor) { + #if TONEMAP == NONE + #if GAMMA == NONE + // convert to linear space + return decodeGamma(gammaColor); + #else + // output gamma space color directly + return gammaColor; + #endif + #else + // apply tonemapping in linear space and output to linear or + // gamma (which is handled by gammaCorrectOutput) + return gammaCorrectOutput(toneMap(decodeGamma(gammaColor))); + #endif +} +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js new file mode 100644 index 00000000000..a0be892e5be --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js @@ -0,0 +1,58 @@ +export default /* glsl */` + +#if SH_BANDS > 0 + +// unpack signed 11 10 11 bits +vec3 unpack111011s(uint bits) { + return vec3((uvec3(bits) >> uvec3(21u, 11u, 0u)) & uvec3(0x7ffu, 0x3ffu, 0x7ffu)) / vec3(2047.0, 1023.0, 2047.0) * 2.0 - 1.0; +} + +// fetch quantized spherical harmonic coefficients +void fetchScale(in uvec4 t, out float scale, out vec3 a, out vec3 b, out vec3 c) { + scale = uintBitsToFloat(t.x); + a = unpack111011s(t.y); + b = unpack111011s(t.z); + c = unpack111011s(t.w); +} + +// fetch quantized spherical harmonic coefficients +void fetch(in uvec4 t, out vec3 a, out vec3 b, out vec3 c, out vec3 d) { + a = unpack111011s(t.x); + b = unpack111011s(t.y); + c = unpack111011s(t.z); + d = unpack111011s(t.w); +} + +void fetch(in uint t, out vec3 a) { + a = unpack111011s(t); +} + +#if SH_BANDS == 1 + uniform highp usampler2D splatSH_1to3; + void readSHData(in SplatSource source, out vec3 sh[3], out float scale) { + fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); + } +#elif SH_BANDS == 2 + uniform highp usampler2D splatSH_1to3; + uniform highp usampler2D splatSH_4to7; + uniform highp usampler2D splatSH_8to11; + void readSHData(in SplatSource source, out vec3 sh[8], out float scale) { + fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); + fetch(texelFetch(splatSH_4to7, source.uv, 0), sh[3], sh[4], sh[5], sh[6]); + fetch(texelFetch(splatSH_8to11, source.uv, 0).x, sh[7]); + } +#else + uniform highp usampler2D splatSH_1to3; + uniform highp usampler2D splatSH_4to7; + uniform highp usampler2D splatSH_8to11; + uniform highp usampler2D splatSH_12to15; + void readSHData(in SplatSource source, out vec3 sh[15], out float scale) { + fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); + fetch(texelFetch(splatSH_4to7, source.uv, 0), sh[3], sh[4], sh[5], sh[6]); + fetch(texelFetch(splatSH_8to11, source.uv, 0), sh[7], sh[8], sh[9], sh[10]); + fetch(texelFetch(splatSH_12to15, source.uv, 0), sh[11], sh[12], sh[13], sh[14]); + } +#endif + +#endif +`; diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatSource.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSource.js new file mode 100644 index 00000000000..14bfc5b79e0 --- /dev/null +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSource.js @@ -0,0 +1,33 @@ +export default /* glsl */` +attribute vec3 vertex_position; // xy: cornerUV, z: render order offset +attribute uint vertex_id_attrib; // render order base + +uniform uint numSplats; // total number of splats +uniform highp usampler2D splatOrder; // per-splat index to source gaussian + +// initialize the splat source structure +bool initSource(out SplatSource source) { + uint w = uint(textureSize(splatOrder, 0).x); + + // calculate splat order + source.order = vertex_id_attrib + uint(vertex_position.z); + + // return if out of range (since the last block of splats may be partially full) + if (source.order >= numSplats) { + return false; + } + + ivec2 orderUV = ivec2(source.order % w, source.order / w); + + // read splat id + source.id = texelFetch(splatOrder, orderUV, 0).r; + + // map id to uv + source.uv = ivec2(source.id % w, source.id / w); + + // get the corner + source.cornerUV = vertex_position.xy; + + return true; +} +`; diff --git a/src/scene/shader-lib/chunks/skybox/frag/skybox.js b/src/scene/shader-lib/chunks/skybox/frag/skybox.js index f0f86741727..68ac668774d 100644 --- a/src/scene/shader-lib/chunks/skybox/frag/skybox.js +++ b/src/scene/shader-lib/chunks/skybox/frag/skybox.js @@ -1,7 +1,7 @@ export default /* glsl */` #include "decodePS" #include "gamma" - #include "tonemapping" + #include "tonemappingPS" #include "envMultiplyPS" varying vec3 vViewDir; diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js index 4b6643cbbf0..20500cb070a 100644 --- a/src/scene/shader-lib/programs/lit-shader.js +++ b/src/scene/shader-lib/programs/lit-shader.js @@ -430,9 +430,6 @@ class LitShader { const lightType = this.shaderPassInfo.lightType; let shadowType = this.shaderPassInfo.shadowType; - const shadowInfo = shadowTypeInfo.get(shadowType); - Debug.assert(shadowInfo); - const isVsm = shadowInfo?.vsm ?? false; // If not a directional light and using clustered, fall back to using PCF3x3 if shadow type isn't supported if (lightType !== LIGHTTYPE_DIRECTIONAL && options.clusteredLightingEnabled) { @@ -441,6 +438,10 @@ class LitShader { } } + const shadowInfo = shadowTypeInfo.get(shadowType); + Debug.assert(shadowInfo); + const isVsm = shadowInfo?.vsm ?? false; + let code = this._fsGetBeginCode(); if (shadowType === SHADOW_VSM_32F) { diff --git a/src/scene/shader-lib/programs/particle.js b/src/scene/shader-lib/programs/particle.js index 1b7613b3961..2279ac69265 100644 --- a/src/scene/shader-lib/programs/particle.js +++ b/src/scene/shader-lib/programs/particle.js @@ -1,5 +1,5 @@ import { ShaderUtils } from '../../../platform/graphics/shader-utils.js'; -import { BLEND_ADDITIVE, BLEND_MULTIPLICATIVE, BLEND_NORMAL } from '../../constants.js'; +import { BLEND_ADDITIVE, BLEND_MULTIPLICATIVE, BLEND_NORMAL, tonemapNames } from '../../constants.js'; import { shaderChunks } from '../chunks/chunks.js'; import { ShaderGenerator } from './shader-generator.js'; @@ -85,7 +85,7 @@ class ShaderGeneratorParticle extends ShaderGenerator { fshader += shaderChunks.decodePS; fshader += ShaderGenerator.gammaCode(options.gamma); - fshader += ShaderGenerator.tonemapCode(options.toneMap); + fshader += '#include "tonemappingPS"\n'; fshader += ShaderGenerator.fogCode(options.fog); if (options.normal === 2) fshader += '\nuniform sampler2D normalMap;\n'; @@ -105,11 +105,20 @@ class ShaderGeneratorParticle extends ShaderGenerator { } fshader += shaderChunks.particle_endPS; + const includes = new Map(Object.entries({ + ...shaderChunks, + ...options.chunks + })); + + const fragmentDefines = new Map(options.defines); + fragmentDefines.set('TONEMAP', tonemapNames[options.toneMap]); + return ShaderUtils.createDefinition(device, { name: 'ParticleShader', vertexCode: vshader, fragmentCode: fshader, - fragmentDefines: options.defines, + fragmentDefines: fragmentDefines, + fragmentIncludes: includes, vertexDefines: options.defines }); } diff --git a/src/scene/shader-lib/programs/shader-generator-shader.js b/src/scene/shader-lib/programs/shader-generator-shader.js index 69211813850..bacbce60187 100644 --- a/src/scene/shader-lib/programs/shader-generator-shader.js +++ b/src/scene/shader-lib/programs/shader-generator-shader.js @@ -1,6 +1,7 @@ import { hashCode } from '../../../core/hash.js'; import { SEMANTIC_ATTR15, SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SHADERLANGUAGE_WGSL } from '../../../platform/graphics/constants.js'; import { ShaderUtils } from '../../../platform/graphics/shader-utils.js'; +import { tonemapNames } from '../../constants.js'; import { ShaderPass } from '../../shader-pass.js'; import { shaderChunks } from '../chunks/chunks.js'; import { ShaderGenerator } from './shader-generator.js'; @@ -14,7 +15,7 @@ const fShader = ` #include "shaderPassDefines" #include "decodePS" #include "gamma" - #include "tonemapping" + #include "tonemappingPS" #include "fog" #include "userCode" `; @@ -109,16 +110,18 @@ class ShaderGeneratorShader extends ShaderGenerator { definitionOptions.fragmentCode = desc.fragmentCode; } else { - const includes = new Map(); - const defines = new Map(options.defines); + const includes = new Map(Object.entries({ + ...shaderChunks, + ...options.chunks + })); includes.set('shaderPassDefines', shaderPassInfo.shaderDefines); - includes.set('decodePS', shaderChunks.decodePS); includes.set('gamma', ShaderGenerator.gammaCode(options.gamma)); - includes.set('tonemapping', ShaderGenerator.tonemapCode(options.toneMapping)); includes.set('fog', ShaderGenerator.fogCode(options.fog)); includes.set('userCode', desc.fragmentCode); - includes.set('pick', shaderChunks.pickPS); + + const defines = new Map(options.defines); + defines.set('TONEMAP', tonemapNames[options.toneMapping]); definitionOptions.fragmentCode = fShader; definitionOptions.fragmentIncludes = includes; diff --git a/src/scene/shader-lib/programs/skybox.js b/src/scene/shader-lib/programs/skybox.js index 8313921bc53..e73221d6415 100644 --- a/src/scene/shader-lib/programs/skybox.js +++ b/src/scene/shader-lib/programs/skybox.js @@ -4,7 +4,7 @@ import { ChunkUtils } from '../chunk-utils.js'; import { ShaderUtils } from '../../../platform/graphics/shader-utils.js'; import { ShaderGenerator } from './shader-generator.js'; -import { SKYTYPE_INFINITE } from '../../constants.js'; +import { SKYTYPE_INFINITE, tonemapNames } from '../../constants.js'; class ShaderGeneratorSkybox extends ShaderGenerator { generateKey(options) { @@ -17,6 +17,7 @@ class ShaderGeneratorSkybox extends ShaderGenerator { // defines const defines = new Map(); + defines.set('TONEMAP', tonemapNames[options.toneMapping]); defines.set('SKYBOX_DECODE_FNC', ChunkUtils.decodeFunc(options.encoding)); if (options.skymesh !== SKYTYPE_INFINITE) defines.set('SKYMESH', ''); if (options.type === 'cubemap') { @@ -24,10 +25,12 @@ class ShaderGeneratorSkybox extends ShaderGenerator { } // includes - const includes = new Map(); + const includes = new Map(Object.entries({ + ...shaderChunks, + ...options.chunks + })); includes.set('decodePS', shaderChunks.decodePS); includes.set('gamma', ShaderGenerator.gammaCode(options.gamma)); - includes.set('tonemapping', ShaderGenerator.tonemapCode(options.toneMapping)); includes.set('envMultiplyPS', shaderChunks.envMultiplyPS); if (options.type !== 'cubemap') {