From 456f8729fd5f53825e1bef8159be97a96fdd9a3f Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Mon, 30 Dec 2024 14:22:43 +0000 Subject: [PATCH 1/5] Handle NaNs in GSplat data (#7243) --- src/scene/gsplat/gsplat-sorter.js | 37 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/scene/gsplat/gsplat-sorter.js b/src/scene/gsplat/gsplat-sorter.js index 539a0012423..a4f15b5e413 100644 --- a/src/scene/gsplat/gsplat-sorter.js +++ b/src/scene/gsplat/gsplat-sorter.js @@ -108,6 +108,9 @@ function SortWorker() { const y = centers[istride + 1] - py; const z = centers[istride + 2] - pz; const d = x * dx + y * dy + z * dz; + if (isNaN(d)) { + continue; + } const sortKey = Math.floor((d - minDist) * divider); distances[i] = sortKey; @@ -160,24 +163,36 @@ function SortWorker() { centers = new Float32Array(message.data.centers); // calculate bounds - boundMin.x = boundMax.x = centers[0]; - boundMin.y = boundMax.y = centers[1]; - boundMin.z = boundMax.z = centers[2]; - + let initialized = false; const numVertices = centers.length / 3; - for (let i = 1; i < numVertices; ++i) { + for (let i = 0; i < numVertices; ++i) { const x = centers[i * 3 + 0]; const y = centers[i * 3 + 1]; const z = centers[i * 3 + 2]; - boundMin.x = Math.min(boundMin.x, x); - boundMin.y = Math.min(boundMin.y, y); - boundMin.z = Math.min(boundMin.z, z); + if (isNaN(x) || isNaN(y) || isNaN(z)) { + continue; + } + + if (!initialized) { + initialized = true; + boundMin.x = boundMax.x = x; + boundMin.y = boundMax.y = y; + boundMin.z = boundMax.z = z; + } else { + boundMin.x = Math.min(boundMin.x, x); + boundMax.x = Math.max(boundMax.x, x); + boundMin.y = Math.min(boundMin.y, y); + boundMax.y = Math.max(boundMax.y, y); + boundMin.z = Math.min(boundMin.z, z); + boundMax.z = Math.max(boundMax.z, z); + } + } - boundMax.x = Math.max(boundMax.x, x); - boundMax.y = Math.max(boundMax.y, y); - boundMax.z = Math.max(boundMax.z, z); + if (!initialized) { + boundMin.x = boundMax.x = boundMin.y = boundMax.y = boundMin.z = boundMax.z = 0; } + forceUpdate = true; } if (message.data.hasOwnProperty('mapping')) { From c1034c3db14777b3e1d107dc0574abd797b1979b Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Fri, 3 Jan 2025 15:25:39 +0000 Subject: [PATCH 2/5] Store PLY header comments (#7246) --- src/framework/parsers/gsplat-resource.js | 10 +++- src/framework/parsers/ply.js | 75 ++++++++++++++---------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/framework/parsers/gsplat-resource.js b/src/framework/parsers/gsplat-resource.js index d462b29118d..4a6cf86154d 100644 --- a/src/framework/parsers/gsplat-resource.js +++ b/src/framework/parsers/gsplat-resource.js @@ -33,14 +33,22 @@ class GSplatResource { */ splat = null; + /** + * @type {string[] | null} + * @ignore + */ + comments = null; + /** * @param {GraphicsDevice} device - The graphics device. * @param {GSplatData} splatData - The splat data. + * @param {string[]} comments - The PLY file header comments * @ignore */ - constructor(device, splatData) { + constructor(device, splatData, comments) { this.device = device; this.splatData = splatData; + this.comments = comments; } destroy() { diff --git a/src/framework/parsers/ply.js b/src/framework/parsers/ply.js index 682d7e6fd83..a536f8414f9 100644 --- a/src/framework/parsers/ply.js +++ b/src/framework/parsers/ply.js @@ -162,12 +162,16 @@ class StreamBuf { // string containing the ply format const parseHeader = (lines) => { const elements = []; + const comments = []; let format; for (let i = 1; i < lines.length; ++i) { const words = lines[i].split(' '); switch (words[0]) { + case 'comment': + comments.push(words.slice(1).join(' ')); + break; case 'format': format = words[1]; break; @@ -196,7 +200,7 @@ const parseHeader = (lines) => { } } - return { elements, format }; + return { elements, format, comments }; }; // return true if the array of elements references a compressed ply file @@ -239,7 +243,7 @@ const isFloatPly = (elements) => { }; // read the data of a compressed ply file -const readCompressedPly = async (streamBuf, elements, littleEndian) => { +const readCompressedPly = async (streamBuf, elements) => { const result = new GSplatCompressedData(); const numChunks = elements[0].count; @@ -294,7 +298,7 @@ const readCompressedPly = async (streamBuf, elements, littleEndian) => { }; // read the data of a floating point ply file -const readFloatPly = async (streamBuf, elements, littleEndian) => { +const readFloatPly = async (streamBuf, elements) => { // calculate the size of an input element record const element = elements[0]; const properties = element.properties; @@ -336,7 +340,7 @@ const readFloatPly = async (streamBuf, elements, littleEndian) => { return new GSplatData(elements); }; -const readGeneralPly = async (streamBuf, elements, littleEndian) => { +const readGeneralPly = async (streamBuf, elements) => { // read and deinterleave the data for (let i = 0; i < elements.length; ++i) { const element = elements[i]; @@ -391,7 +395,7 @@ const readGeneralPly = async (streamBuf, elements, littleEndian) => { * * @param {ReadableStreamDefaultReader} reader - The reader. * @param {Function|null} propertyFilter - Function to filter properties with. - * @returns {Promise} The ply file data. + * @returns {Promise<{ data: GSplatData | GSplatCompressedData, comments: string[] }>} The ply file data. */ const readPly = async (reader, propertyFilter = null) => { /** @@ -464,14 +468,13 @@ const readPly = async (reader, propertyFilter = null) => { // decode buffer header text and split into lines and remove comments const lines = new TextDecoder('ascii') .decode(streamBuf.data.subarray(0, headerLength)) - .split('\n') - .filter(line => !line.startsWith('comment ')); + .split('\n'); // decode header and build element and property list - const { elements, format } = parseHeader(lines); + const { elements, format, comments } = parseHeader(lines); // check format is supported - if (format !== 'binary_little_endian' && format !== 'binary_big_endian') { + if (format !== 'binary_little_endian') { throw new Error('Unsupported ply format'); } @@ -480,29 +483,36 @@ const readPly = async (reader, propertyFilter = null) => { streamBuf.head = headerLength + endHeaderBytes.length; streamBuf.compact(); - // load compressed PLY with fast path - if (isCompressedPly(elements)) { - return await readCompressedPly(streamBuf, elements, format === 'binary_little_endian'); - } + const readData = async () => { + // load compressed PLY with fast path + if (isCompressedPly(elements)) { + return await readCompressedPly(streamBuf, elements); + } - // allocate element storage - elements.forEach((e) => { - e.properties.forEach((p) => { - const storageType = dataTypeMap.get(p.type); - if (storageType) { - const storage = (!propertyFilter || propertyFilter(p.name)) ? new storageType(e.count) : null; - p.storage = storage; - } + // allocate element storage + elements.forEach((e) => { + e.properties.forEach((p) => { + const storageType = dataTypeMap.get(p.type); + if (storageType) { + const storage = (!propertyFilter || propertyFilter(p.name)) ? new storageType(e.count) : null; + p.storage = storage; + } + }); }); - }); - // load float32 PLY with fast path - if (isFloatPly(elements)) { - return await readFloatPly(streamBuf, elements, format === 'binary_little_endian'); - } + // load float32 PLY with fast path + if (isFloatPly(elements)) { + return await readFloatPly(streamBuf, elements); + } - // fallback, general case - return await readGeneralPly(streamBuf, elements, format === 'binary_little_endian'); + // fallback, general case + return await readGeneralPly(streamBuf, elements); + }; + + return { + data: await readData(), + comments + }; }; // by default load everything @@ -543,19 +553,20 @@ class PlyParser { if (!response || !response.body) { callback('Error loading resource', null); } else { - const gsplatData = await readPly(response.body.getReader(), asset.data.elementFilter ?? defaultElementFilter); + const { data, comments } = await readPly(response.body.getReader(), asset.data.elementFilter ?? defaultElementFilter); // reorder data - if (!gsplatData.isCompressed) { + if (!data.isCompressed) { if (asset.data.reorder ?? true) { - gsplatData.reorderData(); + data.reorderData(); } } // construct the resource const resource = new GSplatResource( this.device, - gsplatData.isCompressed && asset.data.decompress ? gsplatData.decompress() : gsplatData + data.isCompressed && asset.data.decompress ? data.decompress() : data, + comments ); callback(null, resource); From 7a7f9d1ee593a7211814abf8e52058566bdf4d26 Mon Sep 17 00:00:00 2001 From: Martin Valigursky <59932779+mvaligursky@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:29:08 +0000 Subject: [PATCH 3/5] [Fix] HDR lightmap baking is working (#7247) Co-authored-by: Martin Valigursky --- src/framework/lightmapper/lightmap-filters.js | 26 ++++++++++-- src/framework/lightmapper/lightmapper.js | 14 ++++--- .../lightmapper/frag/bilateralDeNoise.js | 40 ++++++++++++++----- .../chunks/lightmapper/frag/dilate.js | 24 +++++++---- 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/framework/lightmapper/lightmap-filters.js b/src/framework/lightmapper/lightmap-filters.js index d529bdad399..60386f783f6 100644 --- a/src/framework/lightmapper/lightmap-filters.js +++ b/src/framework/lightmapper/lightmap-filters.js @@ -17,7 +17,7 @@ class LightmapFilters { this.pixelOffset = new Float32Array(2); // denoise is optional and gets created only when needed - this.shaderDenoise = null; + this.shaderDenoise = []; this.sigmas = null; this.constantSigmas = null; this.kernel = null; @@ -35,10 +35,13 @@ class LightmapFilters { this.constantPixelOffset.setValue(this.pixelOffset); } - prepareDenoise(filterRange, filterSmoothness) { + prepareDenoise(filterRange, filterSmoothness, bakeHDR) { - if (!this.shaderDenoise) { - this.shaderDenoise = createShaderFromCode(this.device, shaderChunks.fullscreenQuadVS, shaderChunksLightmapper.bilateralDeNoisePS, 'lmBilateralDeNoise'); + const index = bakeHDR ? 0 : 1; + if (!this.shaderDenoise[index]) { + const name = `lmBilateralDeNoise-${bakeHDR ? 'hdr' : 'rgbm'}`; + const define = bakeHDR ? '#define HDR\n' : ''; + this.shaderDenoise[index] = createShaderFromCode(this.device, shaderChunks.fullscreenQuadVS, define + shaderChunksLightmapper.bilateralDeNoisePS, name); this.sigmas = new Float32Array(2); this.constantSigmas = this.device.scope.resolve('sigmas'); this.constantKernel = this.device.scope.resolve('kernel[0]'); @@ -52,6 +55,21 @@ class LightmapFilters { this.evaluateDenoiseUniforms(filterRange, filterSmoothness); } + getDenoise(bakeHDR) { + const index = bakeHDR ? 0 : 1; + return this.shaderDenoise[index]; + } + + getDilate(device, bakeHDR) { + const index = bakeHDR ? 0 : 1; + if (!this.shaderDilate[index]) { + const name = `lmDilate-${bakeHDR ? 'hdr' : 'rgbm'}`; + const define = bakeHDR ? '#define HDR\n' : ''; + this.shaderDilate[index] = createShaderFromCode(device, shaderChunks.fullscreenQuadVS, define + shaderChunksLightmapper.dilatePS, name); + } + return this.shaderDilate[index]; + } + evaluateDenoiseUniforms(filterRange, filterSmoothness) { function normpdf(x, sigma) { diff --git a/src/framework/lightmapper/lightmapper.js b/src/framework/lightmapper/lightmapper.js index 4b3147b3d44..96e806ca6a5 100644 --- a/src/framework/lightmapper/lightmapper.js +++ b/src/framework/lightmapper/lightmapper.js @@ -129,6 +129,8 @@ class Lightmapper { initBake(device) { + this.bakeHDR = this.scene.lightmapPixelFormat !== PIXELFORMAT_RGBA8; + // only initialize one time if (!this._initCalled) { this._initCalled = true; @@ -251,7 +253,7 @@ class Lightmapper { } else { material.ambient = new Color(0, 0, 0); // don't bake ambient } - material.chunks.basePS = shaderChunks.basePS + (scene.lightmapPixelFormat === PIXELFORMAT_RGBA8 ? '\n#define LIGHTMAP_RGBM\n' : ''); + material.chunks.basePS = shaderChunks.basePS + (this.bakeHDR ? '' : '\n#define LIGHTMAP_RGBM\n'); material.chunks.endPS = bakeLmEndChunk; material.lightMap = this.blackTex; } else { @@ -299,7 +301,7 @@ class Lightmapper { height: size, format: this.scene.lightmapPixelFormat, mipmaps: false, - type: this.scene.lightmapPixelFormat === PIXELFORMAT_RGBA8 ? TEXTURETYPE_RGBM : TEXTURETYPE_DEFAULT, + type: this.bakeHDR ? TEXTURETYPE_DEFAULT : TEXTURETYPE_RGBM, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, @@ -876,12 +878,14 @@ class Lightmapper { postprocessTextures(device, bakeNodes, passCount) { const numDilates2x = 1; // 1 or 2 dilates (depending on filter being enabled) - const dilateShader = this.lightmapFilters.shaderDilate; + const dilateShader = this.lightmapFilters.getDilate(device, this.bakeHDR); + let denoiseShader; // bilateral denoise filter - runs as a first pass, before dilate const filterLightmap = this.scene.lightmapFilterEnabled; if (filterLightmap) { - this.lightmapFilters.prepareDenoise(this.scene.lightmapFilterRange, this.scene.lightmapFilterSmoothness); + this.lightmapFilters.prepareDenoise(this.scene.lightmapFilterRange, this.scene.lightmapFilterSmoothness, this.bakeHDR); + denoiseShader = this.lightmapFilters.getDenoise(this.bakeHDR); } device.setBlendState(BlendState.NOBLEND); @@ -908,7 +912,7 @@ class Lightmapper { this.lightmapFilters.setSourceTexture(lightmap); const bilateralFilterEnabled = filterLightmap && pass === 0 && i === 0; - drawQuadWithShader(device, tempRT, bilateralFilterEnabled ? this.lightmapFilters.shaderDenoise : dilateShader); + drawQuadWithShader(device, tempRT, bilateralFilterEnabled ? denoiseShader : dilateShader); this.lightmapFilters.setSourceTexture(tempTex); drawQuadWithShader(device, nodeRT, dilateShader); diff --git a/src/scene/shader-lib/chunks/lightmapper/frag/bilateralDeNoise.js b/src/scene/shader-lib/chunks/lightmapper/frag/bilateralDeNoise.js index 5d8f6391e05..6f05cee1ac9 100644 --- a/src/scene/shader-lib/chunks/lightmapper/frag/bilateralDeNoise.js +++ b/src/scene/shader-lib/chunks/lightmapper/frag/bilateralDeNoise.js @@ -33,6 +33,22 @@ vec4 encodeRGBM(vec3 color) { // modified RGBM return encoded; } +vec3 decode(vec4 pixel) { + #if HDR + return pixel.rgb; + #else + return decodeRGBM(pixel); + #endif +} + +bool isUsed(vec4 pixel) { + #if HDR + return any(greaterThan(pixel.rgb, vec3(0.0))); + #else + return pixel.a > 0.0; + #endif +} + // filter size #define MSIZE 15 @@ -45,13 +61,13 @@ uniform float kernel[MSIZE]; void main(void) { - vec4 pixelRgbm = texture2DLod(source, vUv0, 0.0); + vec4 pixel = texture2DLod(source, vUv0, 0.0); // lightmap specific optimization - skip pixels that were not baked // this also allows dilate filter that work on the output of this to work correctly, as it depends on .a being zero // to dilate, which the following blur filter would otherwise modify - if (pixelRgbm.a <= 0.0) { - gl_FragColor = pixelRgbm; + if (!isUsed(pixel)) { + gl_FragColor = pixel; return ; } @@ -61,9 +77,9 @@ void main(void) { // domain sigma - controls blurriness based on a pixel similarity (to preserve edges) float bSigma = sigmas.y; - vec3 pixelHdr = decodeRGBM(pixelRgbm); + vec3 pixelHdr = decode(pixel); vec3 accumulatedHdr = vec3(0.0); - float accumulatedFactor = 0.0; + float accumulatedFactor = 0.000001; // avoid division by zero // read out the texels const int kSize = (MSIZE-1)/2; @@ -72,11 +88,11 @@ void main(void) { // sample the pixel with offset vec2 coord = vUv0 + vec2(float(i), float(j)) * pixelOffset; - vec4 rgbm = texture2DLod(source, coord, 0.0); + vec4 pix = texture2DLod(source, coord, 0.0); // lightmap - only use baked pixels - if (rgbm.a > 0.0) { - vec3 hdr = decodeRGBM(rgbm); + if (isUsed(pix)) { + vec3 hdr = decode(pix); // bilateral factors float factor = kernel[kSize + j] * kernel[kSize + i]; @@ -89,6 +105,12 @@ void main(void) { } } - gl_FragColor = encodeRGBM(accumulatedHdr / accumulatedFactor); + vec3 finalHDR = accumulatedHdr / accumulatedFactor; + + #if HDR + gl_FragColor = vec4(finalHDR, 1.0); + #else + gl_FragColor = encodeRGBM(finalHDR); + #endif } `; diff --git a/src/scene/shader-lib/chunks/lightmapper/frag/dilate.js b/src/scene/shader-lib/chunks/lightmapper/frag/dilate.js index 34dfb029995..4ef1044b745 100644 --- a/src/scene/shader-lib/chunks/lightmapper/frag/dilate.js +++ b/src/scene/shader-lib/chunks/lightmapper/frag/dilate.js @@ -5,16 +5,24 @@ varying vec2 vUv0; uniform sampler2D source; uniform vec2 pixelOffset; +bool isUsed(vec4 pixel) { + #if HDR + return any(greaterThan(pixel.rgb, vec3(0.0))); + #else + return pixel.a > 0.0; + #endif +} + void main(void) { vec4 c = texture2DLod(source, vUv0, 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 - pixelOffset, 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(0, -pixelOffset.y), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, -pixelOffset.y), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, 0), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, 0), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, pixelOffset.y), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + vec2(0, pixelOffset.y), 0.0); - c = c.a>0.0? c : texture2DLod(source, vUv0 + pixelOffset, 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 - pixelOffset, 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(0, -pixelOffset.y), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, -pixelOffset.y), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, 0), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, 0), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, pixelOffset.y), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(0, pixelOffset.y), 0.0); + c = isUsed(c) ? c : texture2DLod(source, vUv0 + pixelOffset, 0.0); gl_FragColor = c; } `; From e079d9f3a1a9436bf24f665dd08aaa7932aadc5c Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Mon, 6 Jan 2025 14:32:27 +0200 Subject: [PATCH 4/5] ButtonComponent remove EntityReference & optimize add/remove speed. (#7204) * ButtonComponent remove EntityReference * remove reliance on component system global event lists * lint # Conflicts: # src/framework/components/button/component.js --- src/framework/components/button/component.js | 258 ++++++++++++++----- src/framework/components/button/system.js | 2 +- src/framework/components/element/system.js | 5 + src/framework/components/system.js | 1 + src/framework/entity.js | 5 + 5 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/framework/components/button/component.js b/src/framework/components/button/component.js index d80d9e75905..84de9f1b5a5 100644 --- a/src/framework/components/button/component.js +++ b/src/framework/components/button/component.js @@ -1,7 +1,9 @@ import { now } from '../../../core/time.js'; import { math } from '../../../core/math/math.js'; import { Color } from '../../../core/math/color.js'; -import { EntityReference } from '../../utils/entity-reference.js'; + +import { GraphNode } from '../../../scene/graph-node.js'; + import { Component } from '../component.js'; import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT } from './constants.js'; import { ELEMENTTYPE_GROUP } from '../element/constants.js'; @@ -11,6 +13,7 @@ import { ELEMENTTYPE_GROUP } from '../element/constants.js'; * @import { ButtonComponentData } from './data.js' * @import { ButtonComponentSystem } from './system.js' * @import { Entity } from '../../entity.js' + * @import { EventHandle } from '../../../core/event-handle.js' * @import { Vec4 } from '../../../core/math/vec4.js' */ @@ -265,6 +268,54 @@ class ButtonComponent extends Component { /** @private */ _defaultSpriteFrame = 0; + /** + * @type {Entity|null} + * @private + */ + _imageEntity = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtElementAdd = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementAdd = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementRemove = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementColor = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementOpacity = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementSpriteAsset = null; + + /** + * @type {EventHandle|null} + * @private + */ + _evtImageEntityElementSpriteFrame = null; + /** * Create a new ButtonComponent instance. * @@ -274,14 +325,14 @@ class ButtonComponent extends Component { constructor(system, entity) { super(system, entity); - this._imageReference = new EntityReference(this, 'imageEntity', { - 'element#gain': this._onImageElementGain, - 'element#lose': this._onImageElementLose, - 'element#set:color': this._onSetColor, - 'element#set:opacity': this._onSetOpacity, - 'element#set:spriteAsset': this._onSetSpriteAsset, - 'element#set:spriteFrame': this._onSetSpriteFrame - }); + this._visualState = VisualState.DEFAULT; + this._isHovering = false; + this._hoveringCounter = 0; + this._isPressed = false; + + this._defaultTint = new Color(1, 1, 1, 1); + this._defaultSpriteAsset = null; + this._defaultSpriteFrame = 0; this._toggleLifecycleListeners('on', system); } @@ -337,19 +388,46 @@ class ButtonComponent extends Component { * Sets the entity to be used as the button background. The entity must have an * {@link ElementComponent} configured as an image element. * - * @type {Entity} + * @type {Entity|string|null} */ set imageEntity(arg) { - this._setValue('imageEntity', arg); + if (this._imageEntity !== arg) { + const isString = typeof arg === 'string'; + if (this._imageEntity && isString && this._imageEntity.getGuid() === arg) { + return; + } + + if (this._imageEntity) { + this._imageEntityUnsubscribe(); + } + + if (arg instanceof GraphNode) { + this._imageEntity = arg; + } else if (isString) { + this._imageEntity = this.system.app.getEntityFromIndex(arg) || null; + } else { + this._imageEntity = null; + } + + if (this._imageEntity) { + this._imageEntitySubscribe(); + } + + if (this._imageEntity) { + this.data.imageEntity = this._imageEntity.getGuid(); + } else if (isString && arg) { + this.data.imageEntity = arg; + } + } } /** * Gets the entity to be used as the button background. * - * @type {Entity} + * @type {Entity|null} */ get imageEntity() { - return this.data.imageEntity; + return this._imageEntity; } /** @@ -599,8 +677,12 @@ class ButtonComponent extends Component { this[onOrOff]('set_inactiveSpriteAsset', this._onSetTransitionValue, this); this[onOrOff]('set_inactiveSpriteFrame', this._onSetTransitionValue, this); - system.app.systems.element[onOrOff]('add', this._onElementComponentAdd, this); - system.app.systems.element[onOrOff]('beforeremove', this._onElementComponentRemove, this); + if (onOrOff === 'on') { + this._evtElementAdd = this.entity.on('element:add', this._onElementComponentAdd, this); + } else { + this._evtElementAdd?.off(); + this._evtElementAdd = null; + } } _onSetActive(name, oldValue, newValue) { @@ -623,24 +705,66 @@ class ButtonComponent extends Component { } } - _onElementComponentRemove(entity) { - if (this.entity === entity) { - this._toggleHitElementListeners('off'); + _imageEntitySubscribe() { + this._evtImageEntityElementAdd = this._imageEntity.on('element:add', this._onImageElementGain, this); + + if (this._imageEntity.element) { + this._onImageElementGain(); } } - _onElementComponentAdd(entity) { - if (this.entity === entity) { - this._toggleHitElementListeners('on'); + _imageEntityUnsubscribe() { + this._evtImageEntityElementAdd?.off(); + this._evtImageEntityElementAdd = null; + + if (this._imageEntity?.element) { + this._onImageElementLose(); } } + _imageEntityElementSubscribe() { + const element = this._imageEntity.element; + + this._evtImageEntityElementRemove = element.once('beforeremove', this._onImageElementLose, this); + this._evtImageEntityElementColor = element.on('set:color', this._onSetColor, this); + this._evtImageEntityElementOpacity = element.on('set:opacity', this._onSetOpacity, this); + this._evtImageEntityElementSpriteAsset = element.on('set:spriteAsset', this._onSetSpriteAsset, this); + this._evtImageEntityElementSpriteFrame = element.on('set:spriteFrame', this._onSetSpriteFrame, this); + } + + _imageEntityElementUnsubscribe() { + this._evtImageEntityElementRemove?.off(); + this._evtImageEntityElementRemove = null; + + this._evtImageEntityElementColor?.off(); + this._evtImageEntityElementColor = null; + + this._evtImageEntityElementOpacity?.off(); + this._evtImageEntityElementOpacity = null; + + this._evtImageEntityElementSpriteAsset?.off(); + this._evtImageEntityElementSpriteAsset = null; + + this._evtImageEntityElementSpriteFrame?.off(); + this._evtImageEntityElementSpriteFrame = null; + } + + _onElementComponentRemove() { + this._toggleHitElementListeners('off'); + } + + _onElementComponentAdd() { + this._toggleHitElementListeners('on'); + } + _onImageElementLose() { + this._imageEntityElementUnsubscribe(); this._cancelTween(); this._resetToDefaultVisualState(this.transitionMode); } _onImageElementGain() { + this._imageEntityElementSubscribe(); this._storeDefaultVisualState(); this._forceReapplyVisualState(); } @@ -654,6 +778,7 @@ class ButtonComponent extends Component { return; } + this.entity.element[onOrOff]('beforeremove', this._onElementComponentRemove, this); this.entity.element[onOrOff]('mouseenter', this._onMouseEnter, this); this.entity.element[onOrOff]('mouseleave', this._onMouseLeave, this); this.entity.element[onOrOff]('mousedown', this._onMouseDown, this); @@ -674,15 +799,14 @@ class ButtonComponent extends Component { _storeDefaultVisualState() { // If the element is of group type, all it's visual properties are null - if (this._imageReference.hasComponent('element')) { - const element = this._imageReference.entity.element; - if (element.type !== ELEMENTTYPE_GROUP) { - this._storeDefaultColor(element.color); - this._storeDefaultOpacity(element.opacity); - this._storeDefaultSpriteAsset(element.spriteAsset); - this._storeDefaultSpriteFrame(element.spriteFrame); - } + const element = this._imageEntity?.element; + if (!element || element.type === ELEMENTTYPE_GROUP) { + return; } + this._storeDefaultColor(element.color); + this._storeDefaultOpacity(element.opacity); + this._storeDefaultSpriteAsset(element.spriteAsset); + this._storeDefaultSpriteFrame(element.spriteFrame); } _storeDefaultColor(color) { @@ -896,17 +1020,18 @@ class ButtonComponent extends Component { // image back to its original tint. Note that this happens immediately, i.e. // without any animation. _resetToDefaultVisualState(transitionMode) { - if (this._imageReference.hasComponent('element')) { - switch (transitionMode) { - case BUTTON_TRANSITION_MODE_TINT: - this._cancelTween(); - this._applyTintImmediately(this._defaultTint); - break; - - case BUTTON_TRANSITION_MODE_SPRITE_CHANGE: - this._applySprite(this._defaultSpriteAsset, this._defaultSpriteFrame); - break; - } + if (!this._imageEntity?.element) { + return; + } + switch (transitionMode) { + case BUTTON_TRANSITION_MODE_TINT: + this._cancelTween(); + this._applyTintImmediately(this._defaultTint); + break; + + case BUTTON_TRANSITION_MODE_SPRITE_CHANGE: + this._applySprite(this._defaultSpriteAsset, this._defaultSpriteFrame); + break; } } @@ -923,21 +1048,24 @@ class ButtonComponent extends Component { } _applySprite(spriteAsset, spriteFrame) { - spriteFrame = spriteFrame || 0; + const element = this._imageEntity?.element; + if (!element) { + return; + } - if (this._imageReference.hasComponent('element')) { - this._isApplyingSprite = true; + spriteFrame = spriteFrame || 0; - if (this._imageReference.entity.element.spriteAsset !== spriteAsset) { - this._imageReference.entity.element.spriteAsset = spriteAsset; - } + this._isApplyingSprite = true; - if (this._imageReference.entity.element.spriteFrame !== spriteFrame) { - this._imageReference.entity.element.spriteFrame = spriteFrame; - } + if (element.spriteAsset !== spriteAsset) { + element.spriteAsset = spriteAsset; + } - this._isApplyingSprite = false; + if (element.spriteFrame !== spriteFrame) { + element.spriteFrame = spriteFrame; } + + this._isApplyingSprite = false; } _applyTint(tintColor) { @@ -951,10 +1079,11 @@ class ButtonComponent extends Component { } _applyTintImmediately(tintColor) { + const element = this._imageEntity?.element; if ( !tintColor || - !this._imageReference.hasComponent('element') || - this._imageReference.entity.element.type === ELEMENTTYPE_GROUP + !element || + element.type === ELEMENTTYPE_GROUP ) { return; } @@ -963,29 +1092,30 @@ class ButtonComponent extends Component { this._isApplyingTint = true; - if (!color3.equals(this._imageReference.entity.element.color)) { - this._imageReference.entity.element.color = color3; + if (!color3.equals(element.color)) { + element.color = color3; } - if (this._imageReference.entity.element.opacity !== tintColor.a) { - this._imageReference.entity.element.opacity = tintColor.a; + if (element.opacity !== tintColor.a) { + element.opacity = tintColor.a; } this._isApplyingTint = false; } _applyTintWithTween(tintColor) { + const element = this._imageEntity?.element; if ( !tintColor || - !this._imageReference.hasComponent('element') || - this._imageReference.entity.element.type === ELEMENTTYPE_GROUP + !element || + element.type === ELEMENTTYPE_GROUP ) { return; } const color3 = toColor3(tintColor); - const color = this._imageReference.entity.element.color; - const opacity = this._imageReference.entity.element.opacity; + const color = element.color; + const opacity = element.opacity; if (color3.equals(color) && tintColor.a === opacity) return; @@ -1030,7 +1160,6 @@ class ButtonComponent extends Component { this._hoveringCounter = 0; this._isPressed = false; - this._imageReference.onParentComponentEnable(); this._toggleHitElementListeners('on'); this._forceReapplyVisualState(); } @@ -1041,9 +1170,16 @@ class ButtonComponent extends Component { } onRemove() { + this._imageEntityUnsubscribe(); this._toggleLifecycleListeners('off', this.system); this.onDisable(); } + + resolveDuplicatedEntityReferenceProperties(oldButton, duplicatedIdsMap) { + if (oldButton.imageEntity) { + this.imageEntity = duplicatedIdsMap[oldButton.imageEntity.getGuid()]; + } + } } function toColor3(color4) { diff --git a/src/framework/components/button/system.js b/src/framework/components/button/system.js index 4a3404010d0..926064fb76f 100644 --- a/src/framework/components/button/system.js +++ b/src/framework/components/button/system.js @@ -9,7 +9,6 @@ import { ButtonComponentData } from './data.js'; const _schema = [ 'enabled', 'active', - { name: 'imageEntity', type: 'entity' }, { name: 'hitPadding', type: 'vec4' }, 'transitionMode', { name: 'hoverTint', type: 'rgba' }, @@ -52,6 +51,7 @@ class ButtonComponentSystem extends ComponentSystem { } initializeComponentData(component, data, properties) { + component.imageEntity = data.imageEntity; super.initializeComponentData(component, data, _schema); } diff --git a/src/framework/components/element/system.js b/src/framework/components/element/system.js index 957c7b969af..c8292183d8f 100644 --- a/src/framework/components/element/system.js +++ b/src/framework/components/element/system.js @@ -75,6 +75,7 @@ class ElementComponentSystem extends ComponentSystem { this.defaultImageMaterials = []; + this.on('add', this.onAddComponent, this); this.on('beforeremove', this.onRemoveComponent, this); } @@ -267,6 +268,10 @@ class ElementComponentSystem extends ComponentSystem { } } + onAddComponent(entity, component) { + entity.fire('element:add'); + } + onRemoveComponent(entity, component) { component.onRemove(); } diff --git a/src/framework/components/system.js b/src/framework/components/system.js index c556840c3dc..7ecfb4d4977 100644 --- a/src/framework/components/system.js +++ b/src/framework/components/system.js @@ -75,6 +75,7 @@ class ComponentSystem extends EventHandler { const record = this.store[entity.getGuid()]; const component = entity.c[id]; + component.fire('beforeremove'); this.fire('beforeremove', entity, component); delete this.store[entity.getGuid()]; diff --git a/src/framework/entity.js b/src/framework/entity.js index aebf85c71c1..5d75b917372 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -754,6 +754,11 @@ function resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, oldEntity, n newEntity.render.resolveDuplicatedEntityReferenceProperties(components.render, duplicatedIdsMap); } + // Handle entity button attributes + if (components.button) { + newEntity.button.resolveDuplicatedEntityReferenceProperties(components.button, duplicatedIdsMap); + } + // Handle entity anim attributes if (components.anim) { newEntity.anim.resolveDuplicatedEntityReferenceProperties(components.anim, duplicatedIdsMap); From 0526a94397f334c9df6a88c98f02ca3338cbaab5 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Tue, 7 Jan 2025 09:41:57 +0000 Subject: [PATCH 5/5] Support 1 & 2 bands of SH in uncompressed gsplat (#7248) --- src/scene/gsplat/gsplat-data.js | 21 +++- src/scene/gsplat/gsplat.js | 117 ++++++++++-------- .../shader-lib/chunks/gsplat/vert/gsplatSH.js | 4 +- 3 files changed, 84 insertions(+), 58 deletions(-) diff --git a/src/scene/gsplat/gsplat-data.js b/src/scene/gsplat/gsplat-data.js index ae68d4ca342..a35d6766559 100644 --- a/src/scene/gsplat/gsplat-data.js +++ b/src/scene/gsplat/gsplat-data.js @@ -322,13 +322,22 @@ class GSplatData { return false; } - get hasSHData() { - for (let i = 0; i < 45; ++i) { - if (!this.getProp(`f_rest_${i}`)) { - return false; + // return the number of spherical harmonic bands present. value will be between 0 and 3 inclusive. + get shBands() { + const numProps = () => { + for (let i = 0; i < 45; ++i) { + if (!this.getProp(`f_rest_${i}`)) { + return i; + } } - } - return true; + return 45; + }; + const sizes = { + 9: 1, + 24: 2, + 45: 3 + }; + return sizes[numProps()] ?? 0; } calcMortonOrder() { diff --git a/src/scene/gsplat/gsplat.js b/src/scene/gsplat/gsplat.js index 9fd1d5143ab..36b4187006c 100644 --- a/src/scene/gsplat/gsplat.js +++ b/src/scene/gsplat/gsplat.js @@ -4,7 +4,7 @@ import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Mat3 } from '../../core/math/mat3.js'; import { - ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32U, + ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_R32U, PIXELFORMAT_RGBA32U, PIXELFORMAT_RGBA8 } from '../../platform/graphics/constants.js'; import { Texture } from '../../platform/graphics/texture.js'; @@ -17,9 +17,9 @@ import { createGSplatMaterial } from './gsplat-material.js'; * @import { Material } from '../materials/material.js' */ -const getSHData = (gsplatData) => { +const getSHData = (gsplatData, numCoeffs) => { const result = []; - for (let i = 0; i < 45; ++i) { + for (let i = 0; i < numCoeffs; ++i) { result.push(gsplatData.getProp(`f_rest_${i}`)); } return result; @@ -48,8 +48,8 @@ class GSplat { /** @type {Texture} */ transformBTexture; - /** @type {Boolean} */ - hasSH; + /** @type {0 | 1 | 2 | 3} */ + shBands; /** @type {Texture | undefined} */ sh1to3Texture; @@ -57,6 +57,9 @@ class GSplat { /** @type {Texture | undefined} */ sh4to7Texture; + /** @type {Texture | undefined} */ + sh8Texture; + /** @type {Texture | undefined} */ sh8to11Texture; @@ -90,12 +93,18 @@ class GSplat { this.updateTransformData(gsplatData); // initialize SH data - this.hasSH = gsplatData.hasSHData; - if (this.hasSH) { + this.shBands = gsplatData.shBands; + if (this.shBands > 0) { this.sh1to3Texture = this.createTexture('splatSH_1to3', PIXELFORMAT_RGBA32U, size); - this.sh4to7Texture = this.createTexture('splatSH_4to7', PIXELFORMAT_RGBA32U, size); - this.sh8to11Texture = this.createTexture('splatSH_8to11', PIXELFORMAT_RGBA32U, size); - this.sh12to15Texture = this.createTexture('splatSH_12to15', PIXELFORMAT_RGBA32U, size); + if (this.shBands > 1) { + this.sh4to7Texture = this.createTexture('splatSH_4to7', PIXELFORMAT_RGBA32U, size); + if (this.shBands > 2) { + this.sh8to11Texture = this.createTexture('splatSH_8to11', PIXELFORMAT_RGBA32U, size); + this.sh12to15Texture = this.createTexture('splatSH_12to15', PIXELFORMAT_RGBA32U, size); + } else { + this.sh8Texture = this.createTexture('splatSH_8', PIXELFORMAT_R32U, size); + } + } this.updateSHData(gsplatData); } @@ -107,6 +116,7 @@ class GSplat { this.transformBTexture?.destroy(); this.sh1to3Texture?.destroy(); this.sh4to7Texture?.destroy(); + this.sh8Texture?.destroy(); this.sh8to11Texture?.destroy(); this.sh12to15Texture?.destroy(); } @@ -120,15 +130,12 @@ class GSplat { result.setParameter('transformA', this.transformATexture); result.setParameter('transformB', this.transformBTexture); result.setParameter('numSplats', this.numSplatsVisible); - if (this.hasSH) { - 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); - } + result.setDefine('SH_BANDS', this.shBands); + result.setParameter('splatSH_1to3', this.sh1to3Texture); + result.setParameter('splatSH_4to7', this.sh4to7Texture); + result.setParameter('splatSH_8', this.sh8Texture); + result.setParameter('splatSH_8to11', this.sh8to11Texture); + result.setParameter('splatSH_12to15', this.sh12to15Texture); return result; } @@ -284,11 +291,18 @@ class GSplat { */ updateSHData(gsplatData) { const sh1to3Data = this.sh1to3Texture.lock(); - const sh4to7Data = this.sh4to7Texture.lock(); - const sh8to11Data = this.sh8to11Texture.lock(); - const sh12to15Data = this.sh12to15Texture.lock(); + const sh4to7Data = this.sh4to7Texture?.lock(); + const sh8Data = this.sh8Texture?.lock(); + const sh8to11Data = this.sh8to11Texture?.lock(); + const sh12to15Data = this.sh12to15Texture?.lock(); - const src = getSHData(gsplatData); + const numCoeffs = { + 1: 3, + 2: 8, + 3: 15 + }[this.shBands]; + + const src = getSHData(gsplatData, numCoeffs * 3); const t11 = (1 << 11) - 1; const t10 = (1 << 10) - 1; @@ -297,23 +311,19 @@ class GSplat { const uint32 = new Uint32Array(float32.buffer); // coefficients - const c = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; + const c = new Array(numCoeffs * 3).fill(0); for (let i = 0; i < gsplatData.numSplats; ++i) { // extract coefficients - for (let j = 0; j < 15; ++j) { + for (let j = 0; j < numCoeffs; ++j) { c[j * 3] = src[j][i]; - c[j * 3 + 1] = src[j + 15][i]; - c[j * 3 + 2] = src[j + 30][i]; + c[j * 3 + 1] = src[j + numCoeffs][i]; + c[j * 3 + 2] = src[j + numCoeffs * 2][i]; } // calc maximum value let max = c[0]; - for (let j = 1; j < 45; ++j) { + for (let j = 1; j < numCoeffs * 3; ++j) { max = Math.max(max, Math.abs(c[j])); } @@ -322,7 +332,7 @@ class GSplat { } // normalize - for (let j = 0; j < 15; ++j) { + for (let j = 0; j < numCoeffs; ++j) { c[j * 3 + 0] = Math.max(0, Math.min(t11, Math.floor((c[j * 3 + 0] / max * 0.5 + 0.5) * t11 + 0.5))); c[j * 3 + 1] = Math.max(0, Math.min(t10, Math.floor((c[j * 3 + 1] / max * 0.5 + 0.5) * t10 + 0.5))); c[j * 3 + 2] = Math.max(0, Math.min(t11, Math.floor((c[j * 3 + 2] / max * 0.5 + 0.5) * t11 + 0.5))); @@ -336,26 +346,33 @@ class GSplat { 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]; + if (this.shBands > 1) { + 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]; + + if (this.shBands > 2) { + 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]; + } else { + sh8Data[i] = c[21] << 21 | c[22] << 11 | c[23]; + } + } } this.sh1to3Texture.unlock(); - this.sh4to7Texture.unlock(); - this.sh8to11Texture.unlock(); - this.sh12to15Texture.unlock(); + this.sh4to7Texture?.unlock(); + this.sh8Texture?.unlock(); + this.sh8to11Texture?.unlock(); + this.sh12to15Texture?.unlock(); } } diff --git a/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js index a0be892e5be..a1f968351e5 100644 --- a/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js +++ b/src/scene/shader-lib/chunks/gsplat/vert/gsplatSH.js @@ -35,11 +35,11 @@ void fetch(in uint t, out vec3 a) { #elif SH_BANDS == 2 uniform highp usampler2D splatSH_1to3; uniform highp usampler2D splatSH_4to7; - uniform highp usampler2D splatSH_8to11; + uniform highp usampler2D splatSH_8; 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]); + fetch(texelFetch(splatSH_8, source.uv, 0).x, sh[7]); } #else uniform highp usampler2D splatSH_1to3;