diff --git a/Apps/Sandcastle/gallery/VolumeCloud.html b/Apps/Sandcastle/gallery/VolumeCloud.html index f7f3b632d3cc..68c71d5cbfa2 100644 --- a/Apps/Sandcastle/gallery/VolumeCloud.html +++ b/Apps/Sandcastle/gallery/VolumeCloud.html @@ -327,9 +327,6 @@

Loading...

}`; const fragmentShader = /* glsl */ ` - precision highp float; - precision highp sampler3D; - in vec3 vOrigin; in vec3 vDirection; diff --git a/CHANGES.md b/CHANGES.md index f0d5d81fd449..715808012a11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # Change Log +## 1.137 - 2026-01-02 + +### @cesium/engine + +#### Fixes :wrench: + +- Fix texture coordinates in large billboard collections. [#13042](https://github.com/CesiumGS/cesium/pull/13042) +- Improved voxel memory usage by reworking `Megatexture` to use `Texture3D`. [#12570](https://github.com/CesiumGS/cesium/issues/12570) + ## 1.136 - 2025-12-01 ### @cesium/engine @@ -10,7 +19,6 @@ - Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585) - Fixed depth testing bug with billboards and labels clipping through models [#13012](https://github.com/CesiumGS/cesium/issues/13012) - Fixed unexpected outline artifacts around billboards [#4525](https://github.com/CesiumGS/cesium/issues/4525) -- Fix texture coordinates in large billboard collections [#13042](https://github.com/CesiumGS/cesium/pull/13042) #### Additions :tada: diff --git a/Specs/getWebGLStub.js b/Specs/getWebGLStub.js index dce6bc83ac34..7e5146c615e8 100644 --- a/Specs/getWebGLStub.js +++ b/Specs/getWebGLStub.js @@ -112,6 +112,9 @@ function getWebGLStub(canvas, options) { stub.texParameteri = noop; stub.texImage2D = noop; stub.texSubImage2D = noop; + stub.texStorage3D = noop; + stub.texImage3D = noop; + stub.texSubImage3D = noop; stub.uniform1f = noop; stub.uniform1fv = noop; stub.uniform1i = noop; @@ -232,6 +235,7 @@ function getParameterStub(options) { parameterStubValues[WebGLConstants.MAX_TEXTURE_IMAGE_UNITS] = 16; parameterStubValues[WebGLConstants.MAX_RENDERBUFFER_SIZE] = 16384; parameterStubValues[WebGLConstants.MAX_TEXTURE_SIZE] = 16384; + parameterStubValues[WebGLConstants.MAX_3D_TEXTURE_SIZE] = 2048; parameterStubValues[WebGLConstants.MAX_VARYING_VECTORS] = 30; parameterStubValues[WebGLConstants.MAX_VERTEX_ATTRIBS] = 16; parameterStubValues[WebGLConstants.MAX_VERTEX_TEXTURE_IMAGE_UNITS] = 16; diff --git a/packages/engine/Source/Renderer/Context.js b/packages/engine/Source/Renderer/Context.js index 5b07d8a534d6..8ef9a9458448 100644 --- a/packages/engine/Source/Renderer/Context.js +++ b/packages/engine/Source/Renderer/Context.js @@ -86,32 +86,33 @@ function Context(canvas, options) { ContextLimits._maximumCombinedTextureImageUnits = gl.getParameter( gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS, - ); // min: 8 + ); ContextLimits._maximumCubeMapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE, - ); // min: 16 + ); ContextLimits._maximumFragmentUniformVectors = gl.getParameter( gl.MAX_FRAGMENT_UNIFORM_VECTORS, - ); // min: 16 + ); ContextLimits._maximumTextureImageUnits = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS, - ); // min: 8 + ); ContextLimits._maximumRenderbufferSize = gl.getParameter( gl.MAX_RENDERBUFFER_SIZE, - ); // min: 1 - ContextLimits._maximumTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); // min: 64 + ); + ContextLimits._maximumTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + ContextLimits._maximum3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE); ContextLimits._maximumVaryingVectors = gl.getParameter( gl.MAX_VARYING_VECTORS, - ); // min: 8 + ); ContextLimits._maximumVertexAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS, - ); // min: 8 + ); ContextLimits._maximumVertexTextureImageUnits = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS, - ); // min: 0 + ); ContextLimits._maximumVertexUniformVectors = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS, - ); // min: 128 + ); ContextLimits._maximumSamples = this._webgl2 ? gl.getParameter(gl.MAX_SAMPLES) diff --git a/packages/engine/Source/Renderer/ContextLimits.js b/packages/engine/Source/Renderer/ContextLimits.js index c435971762ac..2744f8736d5d 100644 --- a/packages/engine/Source/Renderer/ContextLimits.js +++ b/packages/engine/Source/Renderer/ContextLimits.js @@ -10,6 +10,7 @@ const ContextLimits = { _maximumTextureImageUnits: 0, _maximumRenderbufferSize: 0, _maximumTextureSize: 0, + _maximum3DTextureSize: 0, _maximumVaryingVectors: 0, _maximumVertexAttributes: 0, _maximumVertexTextureImageUnits: 0, @@ -31,11 +32,11 @@ const ContextLimits = { Object.defineProperties(ContextLimits, { /** * The maximum number of texture units that can be used from the vertex and fragment - * shader with this WebGL implementation. The minimum is eight. If both shaders access the + * shader with this WebGL implementation. The minimum is 32. If both shaders access the * same texture unit, this counts as two texture units. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_COMBINED_TEXTURE_IMAGE_UNITS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_COMBINED_TEXTURE_IMAGE_UNITS. */ maximumCombinedTextureImageUnits: { get: function () { @@ -45,10 +46,10 @@ Object.defineProperties(ContextLimits, { /** * The approximate maximum cube map width and height supported by this WebGL implementation. - * The minimum is 16, but most desktop and laptop implementations will support much larger sizes like 8,192. + * The minimum is 2048, but most desktop and laptop implementations will support much larger sizes like 8,192. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_CUBE_MAP_TEXTURE_SIZE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_CUBE_MAP_TEXTURE_SIZE. */ maximumCubeMapSize: { get: function () { @@ -58,10 +59,10 @@ Object.defineProperties(ContextLimits, { /** * The maximum number of vec4, ivec4, and bvec4 - * uniforms that can be used by a fragment shader with this WebGL implementation. The minimum is 16. + * uniforms that can be used by a fragment shader with this WebGL implementation. The minimum is 224. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_FRAGMENT_UNIFORM_VECTORS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_FRAGMENT_UNIFORM_VECTORS. */ maximumFragmentUniformVectors: { get: function () { @@ -70,10 +71,10 @@ Object.defineProperties(ContextLimits, { }, /** - * The maximum number of texture units that can be used from the fragment shader with this WebGL implementation. The minimum is eight. + * The maximum number of texture units that can be used from the fragment shader with this WebGL implementation. The minimum is 16. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_TEXTURE_IMAGE_UNITS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_TEXTURE_IMAGE_UNITS. */ maximumTextureImageUnits: { get: function () { @@ -83,10 +84,10 @@ Object.defineProperties(ContextLimits, { /** * The maximum renderbuffer width and height supported by this WebGL implementation. - * The minimum is 16, but most desktop and laptop implementations will support much larger sizes like 8,192. + * The minimum is 2048, but most desktop and laptop implementations will support much larger sizes like 8,192. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_RENDERBUFFER_SIZE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_RENDERBUFFER_SIZE. */ maximumRenderbufferSize: { get: function () { @@ -96,10 +97,10 @@ Object.defineProperties(ContextLimits, { /** * The approximate maximum texture width and height supported by this WebGL implementation. - * The minimum is 64, but most desktop and laptop implementations will support much larger sizes like 8,192. + * The minimum is 2048, but most desktop and laptop implementations will support much larger sizes like 8,192. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_TEXTURE_SIZE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_TEXTURE_SIZE. */ maximumTextureSize: { get: function () { @@ -107,12 +108,25 @@ Object.defineProperties(ContextLimits, { }, }, + /** + * The approximate maximum texture width, height, and depth supported by this WebGL implementation. + * The minimum is 256, but most desktop and laptop implementations will support much larger sizes like 2048. + * @memberof ContextLimits + * @type {number} + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_3D_TEXTURE_SIZE. + */ + maximum3DTextureSize: { + get: function () { + return ContextLimits._maximum3DTextureSize; + }, + }, + /** * The maximum number of vec4 varying variables supported by this WebGL implementation. - * The minimum is eight. Matrices and arrays count as multiple vec4s. + * The minimum is 15. Matrices and arrays count as multiple vec4s. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VARYING_VECTORS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VARYING_VECTORS. */ maximumVaryingVectors: { get: function () { @@ -121,10 +135,10 @@ Object.defineProperties(ContextLimits, { }, /** - * The maximum number of vec4 vertex attributes supported by this WebGL implementation. The minimum is eight. + * The maximum number of vec4 vertex attributes supported by this WebGL implementation. The minimum is 16. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VERTEX_ATTRIBS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VERTEX_ATTRIBS. */ maximumVertexAttributes: { get: function () { @@ -134,10 +148,10 @@ Object.defineProperties(ContextLimits, { /** * The maximum number of texture units that can be used from the vertex shader with this WebGL implementation. - * The minimum is zero, which means the GL does not support vertex texture fetch. + * The minimum is 16. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VERTEX_TEXTURE_IMAGE_UNITS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VERTEX_TEXTURE_IMAGE_UNITS. */ maximumVertexTextureImageUnits: { get: function () { @@ -147,10 +161,10 @@ Object.defineProperties(ContextLimits, { /** * The maximum number of vec4, ivec4, and bvec4 - * uniforms that can be used by a vertex shader with this WebGL implementation. The minimum is 16. + * uniforms that can be used by a vertex shader with this WebGL implementation. The minimum is 256. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VERTEX_UNIFORM_VECTORS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VERTEX_UNIFORM_VECTORS. */ maximumVertexUniformVectors: { get: function () { @@ -162,7 +176,7 @@ Object.defineProperties(ContextLimits, { * The minimum aliased line width, in pixels, supported by this WebGL implementation. It will be at most one. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with ALIASED_LINE_WIDTH_RANGE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with ALIASED_LINE_WIDTH_RANGE. */ minimumAliasedLineWidth: { get: function () { @@ -174,7 +188,7 @@ Object.defineProperties(ContextLimits, { * The maximum aliased line width, in pixels, supported by this WebGL implementation. It will be at least one. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with ALIASED_LINE_WIDTH_RANGE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with ALIASED_LINE_WIDTH_RANGE. */ maximumAliasedLineWidth: { get: function () { @@ -186,7 +200,7 @@ Object.defineProperties(ContextLimits, { * The minimum aliased point size, in pixels, supported by this WebGL implementation. It will be at most one. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with ALIASED_POINT_SIZE_RANGE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with ALIASED_POINT_SIZE_RANGE. */ minimumAliasedPointSize: { get: function () { @@ -198,7 +212,7 @@ Object.defineProperties(ContextLimits, { * The maximum aliased point size, in pixels, supported by this WebGL implementation. It will be at least one. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with ALIASED_POINT_SIZE_RANGE. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with ALIASED_POINT_SIZE_RANGE. */ maximumAliasedPointSize: { get: function () { @@ -210,7 +224,7 @@ Object.defineProperties(ContextLimits, { * The maximum supported width of the viewport. It will be at least as large as the visible width of the associated canvas. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VIEWPORT_DIMS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VIEWPORT_DIMS. */ maximumViewportWidth: { get: function () { @@ -222,7 +236,7 @@ Object.defineProperties(ContextLimits, { * The maximum supported height of the viewport. It will be at least as large as the visible height of the associated canvas. * @memberof ContextLimits * @type {number} - * @see {@link https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGet.xml|glGet} with MAX_VIEWPORT_DIMS. + * @see {@link https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml|glGet} with MAX_VIEWPORT_DIMS. */ maximumViewportHeight: { get: function () { diff --git a/packages/engine/Source/Renderer/ShaderSource.js b/packages/engine/Source/Renderer/ShaderSource.js index 16d405fae149..413326414894 100644 --- a/packages/engine/Source/Renderer/ShaderSource.js +++ b/packages/engine/Source/Renderer/ShaderSource.js @@ -153,14 +153,11 @@ function getBuiltinsAndAutomaticUniforms(shaderSource) { } function combineShader(shaderSource, isFragmentShader, context) { - let i; - let length; - // Combine shader sources, generally for pseudo-polymorphism, e.g., czm_getMaterial. let combinedSources = ""; const sources = shaderSource.sources; if (defined(sources)) { - for (i = 0, length = sources.length; i < length; ++i) { + for (let i = 0; i < sources.length; ++i) { // #line needs to be on its own line. combinedSources += `\n#line 0\n${sources[i]}`; } @@ -225,30 +222,34 @@ function combineShader(shaderSource, isFragmentShader, context) { let result = ""; const extensionsLength = extensions.length; - for (i = 0; i < extensionsLength; i++) { + for (let i = 0; i < extensionsLength; i++) { result += extensions[i]; } if (isFragmentShader) { - // If high precision isn't support replace occurrences of highp with mediump - // The highp keyword is not always available on older mobile devices + // If high precision isn't supported, replace occurrences of highp with mediump. + // The highp keyword is not always available on older mobile devices. // See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#In_WebGL_1_highp_float_support_is_optional_in_fragment_shaders - result += - "\ -#ifdef GL_FRAGMENT_PRECISION_HIGH\n\ - precision highp float;\n\ - precision highp int;\n\ -#else\n\ - precision mediump float;\n\ - precision mediump int;\n\ - #define highp mediump\n\ -#endif\n\n"; + result += ` +#ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + precision highp int; +#else + precision mediump float; + precision mediump int; + #define highp mediump +#endif +`; + } + + if (context.webgl2) { + result += `precision highp sampler3D;\n\n`; } // Prepend #defines for uber-shaders const defines = shaderSource.defines; if (defined(defines)) { - for (i = 0, length = defines.length; i < length; ++i) { + for (let i = 0, length = defines.length; i < length; ++i) { const define = defines[i]; if (define.length !== 0) { result += `#define ${define}\n`; diff --git a/packages/engine/Source/Renderer/Texture.js b/packages/engine/Source/Renderer/Texture.js index 314b4432d29c..4b521a720e60 100644 --- a/packages/engine/Source/Renderer/Texture.js +++ b/packages/engine/Source/Renderer/Texture.js @@ -398,7 +398,7 @@ function loadBufferSource(texture, source) { * @param {number} xOffset The texel x coordinate of the lower left corner of the subregion of the texture to be updated. * @param {number} yOffset The texel y coordinate of the lower left corner of the subregion of the texture to be updated. * @param {number} width The width of the source data, in pixels. - * @param {number} width The height of the source data, in pixels. + * @param {number} height The height of the source data, in pixels. * * @private */ diff --git a/packages/engine/Source/Renderer/Texture3D.js b/packages/engine/Source/Renderer/Texture3D.js index 9e6813fa1f85..ec7d0c3b37b1 100644 --- a/packages/engine/Source/Renderer/Texture3D.js +++ b/packages/engine/Source/Renderer/Texture3D.js @@ -68,7 +68,7 @@ function Texture3D(options) { sampler = new Sampler(), } = options; - if (!context.webgl2) { + if (!context.webgl2 && !defined(context.options.getWebGLStub)) { throw new DeveloperError( "WebGL1 does not support texture3D. Please use a WebGL2 context.", ); @@ -113,25 +113,25 @@ function Texture3D(options) { Check.typeOf.number.greaterThan("width", width, 0); - if (width > ContextLimits.maximumTextureSize) { + if (width > ContextLimits.maximum3DTextureSize) { throw new DeveloperError( - `Width must be less than or equal to the maximum texture3D size (${ContextLimits.maximumTextureSize}). Check maximumTextureSize.`, + `Width must be less than or equal to the maximum texture3D size (${ContextLimits.maximum3DTextureSize}). Check maximum3DTextureSize.`, ); } Check.typeOf.number.greaterThan("height", height, 0); - if (height > ContextLimits.maximumTextureSize) { + if (height > ContextLimits.maximum3DTextureSize) { throw new DeveloperError( - `Height must be less than or equal to the maximum texture3D size (${ContextLimits.maximumTextureSize}). Check maximumTextureSize.`, + `Height must be less than or equal to the maximum texture3D size (${ContextLimits.maximum3DTextureSize}). Check maximum3DTextureSize.`, ); } Check.typeOf.number.greaterThan("depth", depth, 0); - if (depth > ContextLimits.maximumTextureSize) { + if (depth > ContextLimits.maximum3DTextureSize) { throw new DeveloperError( - `Depth must be less than or equal to the maximum texture3D size (${ContextLimits.maximumTextureSize}). Check maximumTextureSize.`, + `Depth must be less than or equal to the maximum texture3D size (${ContextLimits.maximum3DTextureSize}). Check maximum3DTextureSize.`, ); } @@ -333,6 +333,164 @@ function loadBufferSource(texture3D, source) { } } +/** + * Copy new image data into this texture, from a source object with width, height, depth, and arrayBufferView properties. + * @param {object} options Object with the following properties: + * @param {object} options.source The source object with width, height, depth, and arrayBufferView properties. + * @param {number} [options.xOffset=0] The offset in the x direction within the texture to copy into. + * @param {number} [options.yOffset=0] The offset in the y direction within the texture to copy into. + * @param {number} [options.zOffset=0] The offset in the z direction within the texture to copy into. + * @param {boolean} [options.skipColorSpaceConversion=false] If true, any custom gamma or color profiles in the texture will be ignored. + * + * @exception {DeveloperError} Cannot call copyFrom with a compressed texture pixel format. + * @exception {DeveloperError} xOffset must be greater than or equal to zero. + * @exception {DeveloperError} yOffset must be greater than or equal to zero. + * @exception {DeveloperError} zOffset must be greater than or equal to zero. + * @exception {DeveloperError} xOffset + source.width must be less than or equal to width. + * @exception {DeveloperError} yOffset + source.height must be less than or equal to height. + * @exception {DeveloperError} zOffset + source.depth must be less than or equal to depth. + * @exception {DeveloperError} This texture was destroyed, i.e., destroy() was called. + * @private + * @example + * texture.copyFrom({ + * source: { + * width : 1, + * height : 1, + * depth : 1, + * arrayBufferView : new Uint8Array([255, 0, 0, 255]) + * } + * }); + */ +Texture3D.prototype.copyFrom = function (options) { + options = options ?? Frozen.EMPTY_OBJECT; + + const { source, xOffset = 0, yOffset = 0, zOffset = 0 } = options; + + //>>includeStart('debug', pragmas.debug); + Check.defined("options.source", source); + Check.defined("options.source.arrayBufferView", source.arrayBufferView); + if (PixelFormat.isCompressedFormat(this._pixelFormat)) { + throw new DeveloperError( + "Cannot call copyFrom with a compressed texture pixel format.", + ); + } + Check.typeOf.number.greaterThanOrEquals("xOffset", xOffset, 0); + Check.typeOf.number.greaterThanOrEquals("yOffset", yOffset, 0); + Check.typeOf.number.greaterThanOrEquals("zOffset", zOffset, 0); + Check.typeOf.number.lessThanOrEquals( + "xOffset + options.source.width", + xOffset + source.width, + this._width, + ); + Check.typeOf.number.lessThanOrEquals( + "yOffset + options.source.height", + yOffset + source.height, + this._height, + ); + Check.typeOf.number.lessThanOrEquals( + "zOffset + options.source.depth", + zOffset + source.depth, + this._depth, + ); + //>>includeEnd('debug'); + + const context = this._context; + const gl = context._gl; + const target = this._textureTarget; + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(target, this._texture); + + const { width, height, depth } = source; + let uploaded = false; + if (!this._initialized) { + if ( + xOffset === 0 && + yOffset === 0 && + zOffset === 0 && + width === this._width && + height === this._height && + depth === this._depth + ) { + loadBufferSource(this, source); + uploaded = true; + } else { + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + loadNull(this); + } + this._initialized = true; + } + + if (!uploaded) { + loadPartialBufferSource( + this, + source.arrayBufferView, + xOffset, + yOffset, + zOffset, + width, + height, + depth, + ); + } + + gl.bindTexture(target, null); +}; + +/** + * Load texel data from a buffer into part of a 3D texture + * + * @param {Texture3D} texture3D The texture3D to which texel values will be loaded. + * @param {TypedArray} arrayBufferView The texel values to be loaded into the texture3D. + * @param {number} xOffset The texel x coordinate of the lower left corner of the subregion of the texture to be updated. + * @param {number} yOffset The texel y coordinate of the lower left corner of the subregion of the texture to be updated. + * @param {number} zOffset The texel z coordinate of the lower left corner of the subregion of the texture to be updated. + * @param {number} width The width of the source data, in pixels. + * @param {number} height The height of the source data, in pixels. + * @param {number} depth The depth of the source data, in pixels. + * + * @private + */ +function loadPartialBufferSource( + texture3D, + arrayBufferView, + xOffset, + yOffset, + zOffset, + width, + height, + depth, +) { + const context = texture3D._context; + const gl = context._gl; + + const { pixelFormat, pixelDatatype } = texture3D; + + const unpackAlignment = PixelFormat.alignmentInBytes( + pixelFormat, + pixelDatatype, + width, + ); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, unpackAlignment); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + + gl.texSubImage3D( + texture3D._textureTarget, + 0, + xOffset, + yOffset, + zOffset, + width, + height, + depth, + pixelFormat, + PixelDatatype.toWebGLConstant(pixelDatatype, context), + arrayBufferView, + ); +} + /** * Compute a dimension of the image for the next mip level. * diff --git a/packages/engine/Source/Scene/Megatexture.js b/packages/engine/Source/Scene/Megatexture.js index 9976f7c0e3ab..3686f7b48160 100644 --- a/packages/engine/Source/Scene/Megatexture.js +++ b/packages/engine/Source/Scene/Megatexture.js @@ -1,17 +1,15 @@ -import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; -import ComponentDatatype from "../Core/ComponentDatatype.js"; +import Check from "../Core/Check.js"; import ContextLimits from "../Renderer/ContextLimits.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; -import CesiumMath from "../Core/Math.js"; import MetadataComponentType from "./MetadataComponentType.js"; import PixelDatatype from "../Renderer/PixelDatatype.js"; import PixelFormat from "../Core/PixelFormat.js"; import RuntimeError from "../Core/RuntimeError.js"; import Sampler from "../Renderer/Sampler.js"; -import Texture from "../Renderer/Texture.js"; +import Texture3D from "../Renderer/Texture3D.js"; import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import TextureWrap from "../Renderer/TextureWrap.js"; @@ -33,52 +31,33 @@ function Megatexture( dimensions, channelCount, componentType, - availableTextureMemoryBytes, + availableTextureMemoryBytes = 134217728, + tileCount, ) { - const maximumTextureMemoryByteLength = 512 * 1024 * 1024; - availableTextureMemoryBytes = Math.min( - availableTextureMemoryBytes ?? 128 * 1024 * 1024, - maximumTextureMemoryByteLength, - ); - // TODO there are a lot of texture packing rules, see https://github.com/CesiumGS/cesium/issues/9572 - // Unsigned short textures not allowed in webgl 1, so treat as float - if (componentType === MetadataComponentType.UNSIGNED_SHORT) { - componentType = MetadataComponentType.FLOAT32; - } - - if ( - componentType === MetadataComponentType.FLOAT32 && - !context.floatingPointTexture - ) { - throw new RuntimeError("Floating point texture not supported"); - } - const pixelDataType = getPixelDataType(componentType); - const pixelFormat = getPixelFormat(channelCount, context.webgl2); - const componentTypeByteLength = - MetadataComponentType.getSizeInBytes(componentType); - const textureDimension = getTextureDimension( + const pixelFormat = getPixelFormat(channelCount); + + const bytesPerSample = + channelCount * MetadataComponentType.getSizeInBytes(componentType); + const textureDimension = Megatexture.get3DTextureDimension( + dimensions, + bytesPerSample, availableTextureMemoryBytes, - channelCount, - componentTypeByteLength, + tileCount, ); + if (Cartesian3.equals(textureDimension, Cartesian3.ZERO)) { + throw new RuntimeError( + "Not enough texture memory available to create a megatexture with the given tile dimensions.", + ); + } - const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.x)); - const sliceCountPerRegionY = Math.ceil(dimensions.z / sliceCountPerRegionX); - const voxelCountPerRegionX = sliceCountPerRegionX * dimensions.x; - const voxelCountPerRegionY = sliceCountPerRegionY * dimensions.y; - const regionCountPerMegatextureX = Math.floor( - textureDimension / voxelCountPerRegionX, - ); - const regionCountPerMegatextureY = Math.floor( - textureDimension / voxelCountPerRegionY, + const tileCounts = Cartesian3.divideComponents( + textureDimension, + dimensions, + new Cartesian3(), ); - if (regionCountPerMegatextureX === 0 || regionCountPerMegatextureY === 0) { - throw new RuntimeError("Tileset is too large to fit into megatexture"); - } - /** * @type {number} * @readonly @@ -96,87 +75,43 @@ function Megatexture( * @readonly */ this.textureMemoryByteLength = - componentTypeByteLength * channelCount * textureDimension ** 2; + bytesPerSample * + textureDimension.x * + textureDimension.y * + textureDimension.z; /** * @type {Cartesian3} * @readonly */ - this.voxelCountPerTile = Cartesian3.clone(dimensions, new Cartesian3()); - - /** - * @type {number} - * @readonly - */ - this.maximumTileCount = - regionCountPerMegatextureX * regionCountPerMegatextureY; - - /** - * @type {Cartesian2} - * @readonly - */ - this.regionCountPerMegatexture = new Cartesian2( - regionCountPerMegatextureX, - regionCountPerMegatextureY, - ); - - /** - * @type {Cartesian2} - * @readonly - */ - this.voxelCountPerRegion = new Cartesian2( - voxelCountPerRegionX, - voxelCountPerRegionY, - ); - - /** - * @type {Cartesian2} - * @readonly - */ - this.sliceCountPerRegion = new Cartesian2( - sliceCountPerRegionX, - sliceCountPerRegionY, - ); - - /** - * @type {Cartesian2} - * @readonly - */ - this.voxelSizeUv = new Cartesian2( - 1.0 / textureDimension, - 1.0 / textureDimension, - ); + this.tileCounts = Cartesian3.clone(tileCounts, new Cartesian3()); /** - * @type {Cartesian2} + * @type {Cartesian3} * @readonly */ - this.sliceSizeUv = new Cartesian2( - dimensions.x / textureDimension, - dimensions.y / textureDimension, - ); + this.voxelCountPerTile = Cartesian3.clone(dimensions, new Cartesian3()); /** - * @type {Cartesian2} + * @type {number} * @readonly */ - this.regionSizeUv = new Cartesian2( - voxelCountPerRegionX / textureDimension, - voxelCountPerRegionY / textureDimension, - ); + this.maximumTileCount = tileCounts.x * tileCounts.y * tileCounts.z; /** - * @type {Texture} + * @type {Texture3D} * @readonly */ - this.texture = new Texture({ + this.texture = new Texture3D({ context: context, pixelFormat: pixelFormat, pixelDatatype: pixelDataType, flipY: false, - width: textureDimension, - height: textureDimension, + width: textureDimension.x, + height: textureDimension.y, + depth: textureDimension.z, sampler: new Sampler({ + wrapR: TextureWrap.CLAMP_TO_EDGE, wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, @@ -184,17 +119,6 @@ function Megatexture( }), }); - const componentDatatype = - MetadataComponentType.toComponentDatatype(componentType); - - /** - * @type {Array} - */ - this.tileVoxelDataTemp = ComponentDatatype.createTypedArray( - componentDatatype, - voxelCountPerRegionX * voxelCountPerRegionY * channelCount, - ); - /** * @type {MegatextureNode[]} * @readonly @@ -229,8 +153,42 @@ function Megatexture( * @readonly */ this.occupiedCount = 0; + + this._nearestSampling = false; } +Object.defineProperties(Megatexture.prototype, { + /** + * Gets or sets the nearest sampling flag. + * @type {boolean} + */ + nearestSampling: { + get: function () { + return this._nearestSampling; + }, + set: function (nearestSampling) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool("nearestSampling", nearestSampling); + //>>includeEnd('debug'); + if (this._nearestSampling === nearestSampling) { + return; + } + if (nearestSampling) { + this.texture.sampler = Sampler.NEAREST; + } else { + this.texture.sampler = new Sampler({ + wrapR: TextureWrap.CLAMP_TO_EDGE, + wrapS: TextureWrap.CLAMP_TO_EDGE, + wrapT: TextureWrap.CLAMP_TO_EDGE, + minificationFilter: TextureMinificationFilter.LINEAR, + magnificationFilter: TextureMagnificationFilter.LINEAR, + }); + } + this._nearestSampling = nearestSampling; + }, + }, +}); + /** * Get the pixel data type to use in a megatexture. * TODO support more @@ -255,49 +213,23 @@ function getPixelDataType(componentType) { * Get the pixel format to use for a megatexture. * * @param {number} channelCount The number of channels in the metadata. Must be 1 to 4. - * @param {boolean} webgl2 true if the context is using webgl2 * @returns {PixelFormat} The pixel format to use for a megatexture. * * @private */ -function getPixelFormat(channelCount, webgl2) { - if (channelCount === 1) { - return webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE; - } else if (channelCount === 2) { - return webgl2 ? PixelFormat.RG : PixelFormat.LUMINANCE_ALPHA; - } else if (channelCount === 3) { - return PixelFormat.RGB; - } else if (channelCount === 4) { - return PixelFormat.RGBA; +function getPixelFormat(channelCount) { + switch (channelCount) { + case 1: + return PixelFormat.RED; + case 2: + return PixelFormat.RG; + case 3: + return PixelFormat.RGB; + case 4: + return PixelFormat.RGBA; } } -/** - * Compute the largest size of a square texture that will fit in the available memory. - * - * @param {number} availableTextureMemoryBytes An upper limit on the texture memory size. - * @param {number} channelCount The number of metadata channels per texel. - * @param {number} componentByteLength The byte length of each component of the metadata. - * @returns {number} The dimension of the square texture to use for the megatexture. - * - * @private - */ -function getTextureDimension( - availableTextureMemoryBytes, - channelCount, - componentByteLength, -) { - // Compute how many texels will fit in the available memory - const texelCount = Math.floor( - availableTextureMemoryBytes / (channelCount * componentByteLength), - ); - // Return the largest power of two texture size that will fit in memory - return Math.min( - ContextLimits.maximumTextureSize, - CesiumMath.previousPowerOfTwo(Math.floor(Math.sqrt(texelCount))), - ); -} - /** * @alias MegatextureNode * @constructor @@ -389,130 +321,173 @@ Megatexture.prototype.isFull = function () { }; /** - * @param {number} tileCount The total number of tiles in the tileset. - * @param {Cartesian3} dimensions The number of voxels in each dimension of the tile. - * @param {number} channelCount The number of channels in the metadata. - * @param {MetadataComponentType} componentType The type of one channel of the metadata. - * @returns {number} + * Compute a 3D texture dimension that contains the given number of tiles, or as many tiles as can fit within the available texture memory. + * @param {Cartesian3} tileDimensions The dimensions of one tile in number of voxels. + * @param {number} bytesPerSample The number of bytes per voxel sample. + * @param {number} availableTextureMemoryBytes An upper limit on the texture memory size in bytes. + * @param {number} [tileCount] The total number of tiles in the tileset. + * @returns {Cartesian3} The computed 3D texture dimensions. */ -Megatexture.getApproximateTextureMemoryByteLength = function ( +Megatexture.get3DTextureDimension = function ( + tileDimensions, + bytesPerSample, + availableTextureMemoryBytes, tileCount, - dimensions, - channelCount, - componentType, ) { - // TODO there's a lot of code duplicate with Megatexture constructor + const textureDimension = new Cartesian3(); + const { maximum3DTextureSize } = ContextLimits; + + // Find the number of tiles we can fit. + const tileSizeInBytes = + bytesPerSample * tileDimensions.x * tileDimensions.y * tileDimensions.z; + const maxTileCount = Math.floor( + availableTextureMemoryBytes / tileSizeInBytes, + ); + if (maxTileCount < 1) { + return textureDimension; + } - // Unsigned short textures not allowed in webgl 1, so treat as float - if (componentType === MetadataComponentType.UNSIGNED_SHORT) { - componentType = MetadataComponentType.FLOAT32; + if (defined(tileCount)) { + tileCount = Math.min(tileCount, maxTileCount); + } else { + tileCount = maxTileCount; } - const datatypeSizeInBytes = - MetadataComponentType.getSizeInBytes(componentType); - const voxelCountTotal = - tileCount * dimensions.x * dimensions.y * dimensions.z; + // Sort the tile dimensions from smallest to largest. + const sortedDimensions = Object.entries(tileDimensions).sort( + (a, b) => a[1] - b[1], + ); - const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.x)); - const sliceCountPerRegionY = Math.ceil(dimensions.z / sliceCountPerRegionX); - const voxelCountPerRegionX = sliceCountPerRegionX * dimensions.x; - const voxelCountPerRegionY = sliceCountPerRegionY * dimensions.y; + // Try arranging all tiles in a single column along the axis of the smallest tile dimension. + const singleColumnLength = sortedDimensions[0][1] * tileCount; + if (singleColumnLength <= maximum3DTextureSize) { + textureDimension[sortedDimensions[0][0]] = singleColumnLength; + textureDimension[sortedDimensions[1][0]] = sortedDimensions[1][1]; + textureDimension[sortedDimensions[2][0]] = sortedDimensions[2][1]; + return textureDimension; + } - // Find the power of two that can fit all tile data, accounting for slices. - // There's probably a non-iterative solution for this, but this is good enough for now. - let textureDimension = CesiumMath.previousPowerOfTwo( - Math.floor(Math.sqrt(voxelCountTotal)), - ); - for (;;) { - const regionCountX = Math.floor(textureDimension / voxelCountPerRegionX); - const regionCountY = Math.floor(textureDimension / voxelCountPerRegionY); - const regionCount = regionCountX * regionCountY; - if (regionCount >= tileCount) { - break; - } else { - textureDimension *= 2; + // Find a nearby number with no prime factor larger than 7. + const factors = findFactorsOfNearbyComposite(tileCount, maxTileCount); + + // Split these factors into the three dimensions, keeping the total texture size + // smaller than the maximum in each dimension. + for (let i = 0; i < 3; i++) { + const [axis, length] = sortedDimensions[i]; + const maxTileCountAlongAxis = Math.floor(maximum3DTextureSize / length); + const axisFactors = getDimensionFromFactors(factors, maxTileCountAlongAxis); + textureDimension[axis] = getProductOfFactors(axisFactors) * length; + // Remove used factors. + for (let j = 0; j < factors.length; j++) { + factors[j] -= axisFactors[j]; } } - const textureMemoryByteLength = - textureDimension * textureDimension * channelCount * datatypeSizeInBytes; - return textureMemoryByteLength; + return textureDimension; }; /** - * Write an array of tile metadata to the megatexture. - * @param {number} index The index of the tile's location in the megatexture. - * @param {Float32Array|Uint16Array|Uint8Array} data The data to be written. + * Approximate an integer by a product of small prime factors (2, 3, 5, and 7). + * @private + * @param {number} n The integer to be approximated + * @param {number} maxN A maximum integer which the approximation should not exceed + * @returns {number[]} The exponents of the prime factors 2, 3, 5, and 7 whose product is the approximation of n */ -Megatexture.prototype.writeDataToTexture = function (index, data) { - // Unsigned short textures not allowed in webgl 1, so treat as float - const tileData = - data.constructor === Uint16Array ? new Float32Array(data) : data; - - const { - tileVoxelDataTemp, - voxelCountPerTile, - sliceCountPerRegion, - voxelCountPerRegion, - channelCount, - regionCountPerMegatexture, - } = this; - - for (let z = 0; z < voxelCountPerTile.z; z++) { - const sliceVoxelOffsetX = (z % sliceCountPerRegion.x) * voxelCountPerTile.x; - const sliceVoxelOffsetY = - Math.floor(z / sliceCountPerRegion.x) * voxelCountPerTile.y; - for (let y = 0; y < voxelCountPerTile.y; y++) { - const readOffset = getReadOffset(voxelCountPerTile, y, z); - const writeOffset = - (sliceVoxelOffsetY + y) * voxelCountPerRegion.x + sliceVoxelOffsetX; - for (let x = 0; x < voxelCountPerTile.x; x++) { - const readIndex = readOffset + x; - const writeIndex = writeOffset + x; - for (let c = 0; c < channelCount; c++) { - tileVoxelDataTemp[writeIndex * channelCount + c] = - tileData[readIndex * channelCount + c]; - } - } - } +function findFactorsOfNearbyComposite(n, maxN) { + n = Math.min(n, maxN); + if (Math.floor(n) !== n) { + throw new DeveloperError("n and maxN must be integers"); + } else if (n < 1) { + throw new DeveloperError("n and maxN must be at least 1"); + } + switch (n) { + case 1: + return [0, 0, 0, 0]; + case 2: + return [1, 0, 0, 0]; + case 3: + return [0, 1, 0, 0]; + } + const log2n = Math.floor(Math.log2(n)); + const previousPowerOfTwo = 2 ** log2n; + const residual = n - previousPowerOfTwo; + const interval = 2 ** (log2n - 2); + let intervalIndex = Math.ceil(residual / interval); + const nextComposite = previousPowerOfTwo + intervalIndex * interval; + if (nextComposite > maxN) { + // Use the previous composite instead + intervalIndex--; + } + switch (intervalIndex) { + case 0: + // previousPowerOfTwo + return [log2n, 0, 0, 0]; + case 1: + // previousPowerOfTwo * 5 / 4 + return [log2n - 2, 0, 1, 0]; + case 2: + // previousPowerOfTwo * 6 / 4 + return [log2n - 1, 1, 0, 0]; + case 3: + // previousPowerOfTwo * 7 / 4 + return [log2n - 2, 0, 0, 1]; + } +} + +function getDimensionFromFactors(factorPowers, maxDimension) { + const maxPowerOfTwo = Math.floor(Math.log2(maxDimension)); + const log2n = Math.min(factorPowers[0], maxPowerOfTwo); + const remainingDimension = maxDimension / 2 ** log2n; + if (remainingDimension >= 7 / 4 && factorPowers[3] > 0) { + return [log2n - 2, 0, 0, 1]; + } else if (remainingDimension >= 6 / 4 && factorPowers[1] > 0) { + return [log2n - 1, 1, 0, 0]; + } else if (remainingDimension >= 5 / 4 && factorPowers[2] > 0) { + return [log2n - 2, 0, 1, 0]; + } + return [log2n, 0, 0, 0]; +} + +function getProductOfFactors(factorPowers) { + let product = 1; + const primeFactors = [2, 3, 5, 7]; + for (let i = 0; i < primeFactors.length; i++) { + product *= primeFactors[i] ** factorPowers[i]; } + return product; +} - const voxelOffsetX = - (index % regionCountPerMegatexture.x) * voxelCountPerRegion.x; - const voxelOffsetY = - Math.floor(index / regionCountPerMegatexture.x) * voxelCountPerRegion.y; +/** + * Write an array of tile metadata to the megatexture. + * @param {number} index The index of the tile's location in the megatexture. + * @param {Float32Array|Uint16Array|Uint8Array} tileData The data to be written. + */ +Megatexture.prototype.writeDataToTexture = function (index, tileData) { + const { tileCounts, voxelCountPerTile } = this; const source = { - arrayBufferView: tileVoxelDataTemp, - width: voxelCountPerRegion.x, - height: voxelCountPerRegion.y, + arrayBufferView: tileData, + width: voxelCountPerTile.x, + height: voxelCountPerTile.y, + depth: voxelCountPerTile.z, }; + const tilesPerZ = tileCounts.x * tileCounts.y; + const iz = Math.floor(index / tilesPerZ); + const remainder = index - iz * tilesPerZ; + const iy = Math.floor(remainder / tileCounts.x); + const ix = remainder - iy * tileCounts.x; + const copyOptions = { source: source, - xOffset: voxelOffsetX, - yOffset: voxelOffsetY, + xOffset: ix * voxelCountPerTile.x, + yOffset: iy * voxelCountPerTile.y, + zOffset: iz * voxelCountPerTile.z, }; this.texture.copyFrom(copyOptions); }; -/** - * Get the offset into the data array for a given row of contiguous voxel data. - * - * @param {Cartesian3} dimensions The number of voxels in each dimension of the tile. - * @param {number} y The y index of the voxel row - * @param {number} z The z index of the voxel row - * @returns {number} The offset into the data array - * @private - */ -function getReadOffset(dimensions, y, z) { - const voxelsPerInputSlice = dimensions.y * dimensions.x; - const sliceIndex = z; - const rowIndex = y; - return sliceIndex * voxelsPerInputSlice + rowIndex * dimensions.x; -} - /** * Returns true if this object was destroyed; otherwise, false. *

diff --git a/packages/engine/Source/Scene/VoxelPrimitive.js b/packages/engine/Source/Scene/VoxelPrimitive.js index 0eb7119ec8e0..1dfe069fd140 100644 --- a/packages/engine/Source/Scene/VoxelPrimitive.js +++ b/packages/engine/Source/Scene/VoxelPrimitive.js @@ -405,11 +405,7 @@ function VoxelPrimitive(options) { octreeLeafNodeTilesPerRow: 0, octreeLeafNodeTexelSizeUv: new Cartesian2(), megatextureTextures: [], - megatextureSliceDimensions: new Cartesian2(), - megatextureTileDimensions: new Cartesian2(), - megatextureVoxelSizeUv: new Cartesian2(), - megatextureSliceSizeUv: new Cartesian2(), - megatextureTileSizeUv: new Cartesian2(), + megatextureTileCounts: new Cartesian3(), dimensions: new Cartesian3(), inputDimensions: new Cartesian3(), paddingBefore: new Cartesian3(), @@ -855,10 +851,7 @@ Object.defineProperties(VoxelPrimitive.prototype, { Check.typeOf.bool("nearestSampling", nearestSampling); //>>includeEnd('debug'); - if (this._nearestSampling !== nearestSampling) { - this._nearestSampling = nearestSampling; - this._shaderDirty = true; - } + this._nearestSampling = nearestSampling; }, }, @@ -1282,6 +1275,7 @@ VoxelPrimitive.prototype.update = function (frameState) { ); uniforms.stepSize = this._stepSizeMultiplier; + updateNearestSampling(this); updateRenderBoundPlanes(this, frameState); // Render the primitive @@ -1294,6 +1288,13 @@ VoxelPrimitive.prototype.update = function (frameState) { frameState.commandList.push(command); }; +function updateNearestSampling(primitive) { + const { megatextures } = primitive._traversal; + for (let i = 0; i < megatextures.length; ++i) { + megatextures[i].nearestSampling = primitive._nearestSampling; + } +} + function updateRenderBoundPlanes(primitive, frameState) { const uniforms = primitive._uniforms; const { renderBoundPlanes } = primitive._shape; @@ -1689,33 +1690,15 @@ function setTraversalUniforms(traversal, uniforms) { ); uniforms.octreeInternalNodeTilesPerRow = traversal.internalNodeTilesPerRow; - const megatextures = traversal.megatextures; + const { megatextures } = traversal; const megatexture = megatextures[0]; - const megatextureLength = megatextures.length; - uniforms.megatextureTextures = new Array(megatextureLength); - for (let i = 0; i < megatextureLength; i++) { + uniforms.megatextureTextures = new Array(megatextures.length); + for (let i = 0; i < megatextures.length; i++) { uniforms.megatextureTextures[i] = megatextures[i].texture; } - - uniforms.megatextureSliceDimensions = Cartesian2.clone( - megatexture.sliceCountPerRegion, - uniforms.megatextureSliceDimensions, - ); - uniforms.megatextureTileDimensions = Cartesian2.clone( - megatexture.regionCountPerMegatexture, - uniforms.megatextureTileDimensions, - ); - uniforms.megatextureVoxelSizeUv = Cartesian2.clone( - megatexture.voxelSizeUv, - uniforms.megatextureVoxelSizeUv, - ); - uniforms.megatextureSliceSizeUv = Cartesian2.clone( - megatexture.sliceSizeUv, - uniforms.megatextureSliceSizeUv, - ); - uniforms.megatextureTileSizeUv = Cartesian2.clone( - megatexture.regionSizeUv, - uniforms.megatextureTileSizeUv, + uniforms.megatextureTileCounts = Cartesian3.clone( + megatexture.tileCounts, + uniforms.megatextureTileCounts, ); } diff --git a/packages/engine/Source/Scene/VoxelRenderResources.js b/packages/engine/Source/Scene/VoxelRenderResources.js index ab7b6f46308c..ec61c926a4c5 100644 --- a/packages/engine/Source/Scene/VoxelRenderResources.js +++ b/packages/engine/Source/Scene/VoxelRenderResources.js @@ -62,7 +62,7 @@ function VoxelRenderResources(primitive) { // The reason this uniform is added by shader builder is because some of the // dynamically generated shader code reads from it. shaderBuilder.addUniform( - "sampler2D", + "sampler3D", "u_megatextureTextures[METADATA_COUNT]", ShaderDestination.FRAGMENT, ); @@ -241,13 +241,6 @@ function VoxelRenderResources(primitive) { ShaderDestination.FRAGMENT, ); } - if (primitive._nearestSampling) { - shaderBuilder.addDefine( - "NEAREST_SAMPLING", - undefined, - ShaderDestination.FRAGMENT, - ); - } const traversal = primitive._traversal; shaderBuilder.addDefine( "SAMPLE_COUNT", diff --git a/packages/engine/Source/Scene/VoxelTraversal.js b/packages/engine/Source/Scene/VoxelTraversal.js index 1a8e971d96f3..c2d1958adc8a 100644 --- a/packages/engine/Source/Scene/VoxelTraversal.js +++ b/packages/engine/Source/Scene/VoxelTraversal.js @@ -28,7 +28,7 @@ import VoxelMetadataOrder from "./VoxelMetadataOrder.js"; * @param {VoxelPrimitive} primitive The voxel primitive for which this traversal will be used. * @param {Context} context The context in which to create GPU resources. * @param {number} keyframeCount The number of keyframes in the tileset. - * @param {number} [maximumTextureMemoryByteLength] The maximum amount of memory to use for textures. + * @param {number} [maximumTextureMemoryByteLength=536870912] The maximum amount of memory to use for textures. * * @private */ @@ -38,32 +38,28 @@ function VoxelTraversal( keyframeCount, maximumTextureMemoryByteLength, ) { - const { provider, dimensions, paddingBefore, paddingAfter } = primitive; + const { provider, dimensions } = primitive; const { types, componentTypes, metadataOrder } = provider; - const inputDimensions = Cartesian3.add( - dimensions, - paddingBefore, + // Adjust input tile dimensions for metadata order + const inputDimensions = Cartesian3.clone( + primitive.inputDimensions, new Cartesian3(), ); - Cartesian3.add(inputDimensions, paddingAfter, inputDimensions); - if (metadataOrder === VoxelMetadataOrder.Y_UP) { const inputDimensionsY = inputDimensions.y; inputDimensions.y = inputDimensions.z; inputDimensions.z = inputDimensionsY; } - if ( - !defined(maximumTextureMemoryByteLength) && - defined(provider.maximumTileCount) - ) { - maximumTextureMemoryByteLength = getApproximateTextureMemoryByteLength( - provider.maximumTileCount, - inputDimensions, - types, - componentTypes, + // TODO: refine this. If provider.maximumTileCount is not defined, we will always allocate 512 MB per metadata property. + if (defined(maximumTextureMemoryByteLength)) { + maximumTextureMemoryByteLength = Math.min( + Math.max(0, maximumTextureMemoryByteLength), + 512 * 1024 * 1024, ); + } else { + maximumTextureMemoryByteLength = 512 * 1024 * 1024; } /** @@ -84,6 +80,12 @@ function VoxelTraversal( */ this.megatextures = new Array(types.length); + const providerTileCount = defined(provider.maximumTileCount) + ? provider.maximumTileCount + : defined(provider.availableLevels) + ? Math.pow(8, provider.availableLevels - 1) + : undefined; + // TODO make sure to split the maximumTextureMemoryByteLength across all the megatextures for (let i = 0; i < types.length; i++) { const type = types[i]; @@ -96,12 +98,14 @@ function VoxelTraversal( componentCount, componentType, maximumTextureMemoryByteLength, + providerTileCount, ); this.textureMemoryByteLength += this.megatextures[i].textureMemoryByteLength; } + // TODO: this number actually varies per megatexture, depending on bytes per sample! const maximumTileCount = this.megatextures[0].maximumTileCount; /** @@ -1228,37 +1232,4 @@ function copyToLeafNodeTexture(data, texelsPerTile, tilesPerRow, texture) { texture.copyFrom(copyOptions); } -/** - * @param {number} tileCount - * @param {Cartesian3} dimensions - * @param {MetadataType[]} types - * @param {MetadataComponentType[]} componentTypes - * @private - */ -function getApproximateTextureMemoryByteLength( - tileCount, - dimensions, - types, - componentTypes, -) { - let textureMemoryByteLength = 0; - - const length = types.length; - for (let i = 0; i < length; i++) { - const type = types[i]; - const componentType = componentTypes[i]; - const componentCount = MetadataType.getComponentCount(type); - - textureMemoryByteLength += - Megatexture.getApproximateTextureMemoryByteLength( - tileCount, - dimensions, - componentCount, - componentType, - ); - } - - return textureMemoryByteLength; -} - export default VoxelTraversal; diff --git a/packages/engine/Source/Scene/processVoxelProperties.js b/packages/engine/Source/Scene/processVoxelProperties.js index 3fff9cbfccc4..eb9f08270ed2 100644 --- a/packages/engine/Source/Scene/processVoxelProperties.js +++ b/packages/engine/Source/Scene/processVoxelProperties.js @@ -294,7 +294,7 @@ function processVoxelProperties(renderResources, primitive) { const functionId = "getPropertiesFromMegatextureAtUv"; shaderBuilder.addFunction( functionId, - `${propertiesStructName} getPropertiesFromMegatextureAtUv(vec2 texcoord)`, + `${propertiesStructName} getPropertiesFromMegatextureAtUv(vec3 texcoord)`, ShaderDestination.FRAGMENT, ); shaderBuilder.addFunctionLines(functionId, [ diff --git a/packages/engine/Source/Shaders/Voxels/IntersectionUtils.glsl b/packages/engine/Source/Shaders/Voxels/IntersectionUtils.glsl index e2797bf4b9a0..3a4e0a02aa9d 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectionUtils.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectionUtils.glsl @@ -135,7 +135,7 @@ RayShapeIntersection nextIntersection(inout Intersections ix) { surfaceIntersection = ix.intersections[i]; int intersectionType = int(length(surfaceIntersection.xyz) - 0.5); bool currShapeIsPositive = intersectionType < 2; - bool enter = intMod(intersectionType, 2) == 0; + bool enter = intersectionType % 2 == 0; ix.surroundCount += enter ? +1 : -1; ix.surroundIsPositive = currShapeIsPositive ? enter : ix.surroundIsPositive; diff --git a/packages/engine/Source/Shaders/Voxels/Megatexture.glsl b/packages/engine/Source/Shaders/Voxels/Megatexture.glsl index 360a6f293fac..b13aac5ec9cb 100644 --- a/packages/engine/Source/Shaders/Voxels/Megatexture.glsl +++ b/packages/engine/Source/Shaders/Voxels/Megatexture.glsl @@ -1,104 +1,36 @@ -// See Octree.glsl for the definitions of SampleData and intMod +// See Octree.glsl for the definitions of SampleData /* Megatexture defines (set in Scene/VoxelRenderResources.js) #define SAMPLE_COUNT ### -#define NEAREST_SAMPLING #define PADDING */ -uniform ivec2 u_megatextureSliceDimensions; // number of slices per tile, in two dimensions -uniform ivec2 u_megatextureTileDimensions; // number of tiles per megatexture, in two dimensions -uniform vec2 u_megatextureVoxelSizeUv; -uniform vec2 u_megatextureSliceSizeUv; -uniform vec2 u_megatextureTileSizeUv; +uniform ivec3 u_megatextureTileCounts; // number of tiles in the megatexture, along each axis -// Integer min, max, clamp: For WebGL1 only -int intMin(int a, int b) { - return a <= b ? a : b; -} -int intMax(int a, int b) { - return a >= b ? a : b; -} -int intClamp(int v, int minVal, int maxVal) { - return intMin(intMax(v, minVal), maxVal); -} - -vec2 index1DTo2DTexcoord(int index, ivec2 dimensions, vec2 uvScale) +vec3 index1DTo3DTexCoord(int index) { - int indexX = intMod(index, dimensions.x); - int indexY = index / dimensions.x; - return vec2(indexX, indexY) * uvScale; + int tilesPerZ = u_megatextureTileCounts.x * u_megatextureTileCounts.y; + int iz = index / tilesPerZ; + int remainder = index - iz * tilesPerZ; + int iy = remainder / u_megatextureTileCounts.x; + int ix = remainder - iy * u_megatextureTileCounts.x; + return vec3(ix, iy, iz) / vec3(u_megatextureTileCounts); } -/* - How is 3D data stored in a 2D megatexture? - - In this example there is only one loaded tile and it has 2x2x2 voxels (8 voxels total). - The data is sliced by Z. The data at Z = 0 is placed in texels (0,0), (0,1), (1,0), (1,1) and - the data at Z = 1 is placed in texels (2,0), (2,1), (3,0), (3,1). - Note that there could be empty space in the megatexture because it's a power of two. - - 0 1 2 3 - +---+---+---+---+ - | | | | | 3 - +---+---+---+---+ - | | | | | 2 - +-------+-------+ - |010|110|011|111| 1 - |--- ---|--- ---| - |000|100|001|101| 0 - +-------+-------+ - - When doing linear interpolation the megatexture needs to be sampled twice: once for - the Z slice above the voxel coordinate and once for the slice below. The two slices - are interpolated with fract(coord.z - 0.5). For example, a Z coordinate of 1.0 is - halfway between two Z slices so the interpolation factor is 0.5. Below is a side view - of the 3D voxel grid with voxel coordinates on the left side. - - 2 +---+ - |001| - 1 +-z-+ - |000| - 0 +---+ - - When doing nearest neighbor the megatexture only needs to be sampled once at the closest Z slice. -*/ - Properties getPropertiesFromMegatexture(in SampleData sampleData) { int tileIndex = sampleData.megatextureIndex; vec3 voxelCoord = sampleData.inputCoordinate; - #if defined(NEAREST_SAMPLING) - // Round to the center of the nearest voxel - voxelCoord = floor(voxelCoord) + vec3(0.5); - #endif - // Tile location - vec2 tileUvOffset = index1DTo2DTexcoord(tileIndex, u_megatextureTileDimensions, u_megatextureTileSizeUv); - - // Slice location - float slice = voxelCoord.z - 0.5; - int sliceIndex = int(floor(slice)); - int sliceIndex0 = intClamp(sliceIndex, 0, u_inputDimensions.z - 1); - vec2 sliceUvOffset0 = index1DTo2DTexcoord(sliceIndex0, u_megatextureSliceDimensions, u_megatextureSliceSizeUv); + // UV coordinate of the lower corner of the tile in the megatexture + vec3 tileUvOffset = index1DTo3DTexCoord(tileIndex); // Voxel location - vec2 voxelUvOffset = clamp(voxelCoord.xy, vec2(0.5), vec2(u_inputDimensions.xy) - vec2(0.5)) * u_megatextureVoxelSizeUv; - - // Final location in the megatexture - vec2 uv0 = tileUvOffset + sliceUvOffset0 + voxelUvOffset; + vec3 tileDimensions = vec3(u_inputDimensions); + vec3 clampedVoxelCoord = clamp(voxelCoord, vec3(0.5), tileDimensions - vec3(0.5)); + vec3 voxelUv = clampedVoxelCoord / tileDimensions / vec3(u_megatextureTileCounts); - #if defined(NEAREST_SAMPLING) - return getPropertiesFromMegatextureAtUv(uv0); - #else - float sliceLerp = fract(slice); - int sliceIndex1 = intMin(sliceIndex + 1, u_inputDimensions.z - 1); - vec2 sliceUvOffset1 = index1DTo2DTexcoord(sliceIndex1, u_megatextureSliceDimensions, u_megatextureSliceSizeUv); - vec2 uv1 = tileUvOffset + sliceUvOffset1 + voxelUvOffset; - Properties properties0 = getPropertiesFromMegatextureAtUv(uv0); - Properties properties1 = getPropertiesFromMegatextureAtUv(uv1); - return mixProperties(properties0, properties1, sliceLerp); - #endif + return getPropertiesFromMegatextureAtUv(tileUvOffset + voxelUv); } // Convert an array of sample datas to a final weighted properties. diff --git a/packages/engine/Source/Shaders/Voxels/Octree.glsl b/packages/engine/Source/Shaders/Voxels/Octree.glsl index a875f9aa9093..5a71a67be35d 100644 --- a/packages/engine/Source/Shaders/Voxels/Octree.glsl +++ b/packages/engine/Source/Shaders/Voxels/Octree.glsl @@ -44,10 +44,6 @@ struct SampleData { #endif }; -// Integer mod: For WebGL1 only -int intMod(in int a, in int b) { - return a - (b * (a / b)); -} int normU8_toInt(in float value) { return int(value * 255.0); } @@ -69,14 +65,14 @@ OctreeNodeData getOctreeNodeData(in vec2 octreeUv) { OctreeNodeData getOctreeChildData(in int parentOctreeIndex, in ivec3 childCoord) { int childIndex = childCoord.z * 4 + childCoord.y * 2 + childCoord.x; - int octreeCoordX = intMod(parentOctreeIndex, u_octreeInternalNodeTilesPerRow) * 9 + 1 + childIndex; + int octreeCoordX = (parentOctreeIndex % u_octreeInternalNodeTilesPerRow) * 9 + 1 + childIndex; int octreeCoordY = parentOctreeIndex / u_octreeInternalNodeTilesPerRow; vec2 octreeUv = u_octreeInternalNodeTexelSizeUv * vec2(float(octreeCoordX) + 0.5, float(octreeCoordY) + 0.5); return getOctreeNodeData(octreeUv); } int getOctreeParentIndex(in int octreeIndex) { - int octreeCoordX = intMod(octreeIndex, u_octreeInternalNodeTilesPerRow) * 9; + int octreeCoordX = (octreeIndex % u_octreeInternalNodeTilesPerRow) * 9; int octreeCoordY = octreeIndex / u_octreeInternalNodeTilesPerRow; vec2 octreeUv = u_octreeInternalNodeTexelSizeUv * vec2(float(octreeCoordX) + 0.5, float(octreeCoordY) + 0.5); vec4 parentData = texture(u_octreeInternalNodeTexture, octreeUv); @@ -139,7 +135,7 @@ void getOctreeLeafSampleDatas(in OctreeNodeData data, in ivec4 octreeCoords, out int leafIndex = data.data; int leafNodeTexelCount = 2; // Adding 0.5 moves to the center of the texel - float leafCoordXStart = float(intMod(leafIndex, u_octreeLeafNodeTilesPerRow) * leafNodeTexelCount) + 0.5; + float leafCoordXStart = float((leafIndex % u_octreeLeafNodeTilesPerRow) * leafNodeTexelCount) + 0.5; float leafCoordY = float(leafIndex / u_octreeLeafNodeTilesPerRow) + 0.5; // Get an interpolation weight and a flag to determine whether to read the parent texture diff --git a/packages/engine/Specs/Renderer/ContextSpec.js b/packages/engine/Specs/Renderer/ContextSpec.js index db0a01b59da2..a5228d9e885a 100644 --- a/packages/engine/Specs/Renderer/ContextSpec.js +++ b/packages/engine/Specs/Renderer/ContextSpec.js @@ -44,48 +44,54 @@ describe( it("get maximumCombinedTextureImageUnits", function () { expect( ContextLimits.maximumCombinedTextureImageUnits, - ).toBeGreaterThanOrEqual(8); + ).toBeGreaterThanOrEqual(32); }); it("get maximumCubeMapSize", function () { - expect(ContextLimits.maximumCubeMapSize).toBeGreaterThanOrEqual(16); + expect(ContextLimits.maximumCubeMapSize).toBeGreaterThanOrEqual(2048); }); it("get maximumFragmentUniformVectors", function () { expect( ContextLimits.maximumFragmentUniformVectors, - ).toBeGreaterThanOrEqual(16); + ).toBeGreaterThanOrEqual(224); }); it("get maximumTextureImageUnits", function () { - expect(ContextLimits.maximumTextureImageUnits).toBeGreaterThanOrEqual(8); + expect(ContextLimits.maximumTextureImageUnits).toBeGreaterThanOrEqual(16); }); it("get maximumRenderbufferSize", function () { - expect(ContextLimits.maximumRenderbufferSize).toBeGreaterThanOrEqual(1); + expect(ContextLimits.maximumRenderbufferSize).toBeGreaterThanOrEqual( + 2048, + ); }); it("get maximumTextureSize", function () { - expect(ContextLimits.maximumTextureSize).toBeGreaterThanOrEqual(64); + expect(ContextLimits.maximumTextureSize).toBeGreaterThanOrEqual(2048); + }); + + it("get maximum3DTextureSize", function () { + expect(ContextLimits.maximum3DTextureSize).toBeGreaterThanOrEqual(256); }); it("get maximumVaryingVectors", function () { - expect(ContextLimits.maximumVaryingVectors).toBeGreaterThanOrEqual(8); + expect(ContextLimits.maximumVaryingVectors).toBeGreaterThanOrEqual(15); }); it("get maximumVertexAttributes", function () { - expect(ContextLimits.maximumVertexAttributes).toBeGreaterThanOrEqual(8); + expect(ContextLimits.maximumVertexAttributes).toBeGreaterThanOrEqual(16); }); it("get maximumVertexTextureImageUnits", function () { expect( ContextLimits.maximumVertexTextureImageUnits, - ).toBeGreaterThanOrEqual(0); + ).toBeGreaterThanOrEqual(16); }); it("get maximumVertexUniformVectors", function () { expect(ContextLimits.maximumVertexUniformVectors).toBeGreaterThanOrEqual( - 1, + 256, ); }); diff --git a/packages/engine/Specs/Renderer/Texture3DSpec.js b/packages/engine/Specs/Renderer/Texture3DSpec.js index aa40d6dc5e0f..2751f2284be1 100644 --- a/packages/engine/Specs/Renderer/Texture3DSpec.js +++ b/packages/engine/Specs/Renderer/Texture3DSpec.js @@ -1,4 +1,5 @@ import { + Cartesian3, Color, PixelFormat, ClearCommand, @@ -26,13 +27,18 @@ describe("Renderer/Texture3D", function () { const fs = ` precision highp sampler3D; uniform sampler3D u_texture; - void main() { out_FragColor = texture(u_texture, vec3(0.0)); } + uniform vec3 u_textureCoordinate; + void main() { out_FragColor = texture(u_texture, u_textureCoordinate); } `; let texture; + const textureCoordinate = new Cartesian3(0.0, 0.0, 0.0); const uniformMap = { u_texture: function () { return texture; }, + u_textureCoordinate: function () { + return textureCoordinate; + }, }; beforeAll(function () { @@ -52,6 +58,9 @@ describe("Renderer/Texture3D", function () { height: size, depth: size, }; + textureCoordinate.x = 0.0; + textureCoordinate.y = 0.0; + textureCoordinate.z = 0.0; }); afterEach(function () { @@ -201,6 +210,42 @@ describe("Renderer/Texture3D", function () { ); }); + it("can copy into a subregion from a typed array", function () { + texture = new Texture3D({ + context: context, + source: source, + sampler: Sampler.NEAREST, + }); + + // Copy a navy voxel into the (0, 0, 0) position + const bytes = new Uint8Array(Color.NAVY.toBytes()); + texture.copyFrom({ + source: { + width: 1, + height: 1, + depth: 1, + arrayBufferView: bytes, + }, + }); + + // Voxel at (0, 0, 0) should be navy + expect({ + context: context, + fragmentShader: fs, + uniformMap: uniformMap, + }).contextToRender(Color.NAVY.toBytes()); + + // Other voxels should still be white + textureCoordinate.x = 1.0; + textureCoordinate.y = 1.0; + textureCoordinate.z = 1.0; + expect({ + context: context, + fragmentShader: fs, + uniformMap: uniformMap, + }).contextToRender(Color.WHITE.toBytes()); + }); + it("can be destroyed", function () { if (!context.webgl2) { return; diff --git a/packages/engine/Specs/Scene/MegatextureSpec.js b/packages/engine/Specs/Scene/MegatextureSpec.js index 1e191000f158..f563b31cfea7 100644 --- a/packages/engine/Specs/Scene/MegatextureSpec.js +++ b/packages/engine/Specs/Scene/MegatextureSpec.js @@ -1,5 +1,6 @@ import { Cartesian3, + ContextLimits, Megatexture, MetadataComponentType, RuntimeError, @@ -38,6 +39,67 @@ describe("Scene/Megatexture", function () { expect(megatexture.voxelCountPerTile).toEqual(dimensions); }); + it("3D texture dimensions are constrained by memory limit", function () { + const dimensions = new Cartesian3(23, 7, 13); + const bytesPerSample = 4; + const tileCount = 8 ** 4; // LODs 0 through 4 fully populated + const availableTextureMemoryBytes = 16 * 1024 * 1024; + + // For consistent test results, temporarily set maximum 3D texture size + // to a value supported on all WebGL2 devices. + const maximum3DTextureSize = ContextLimits.maximum3DTextureSize; + ContextLimits._maximum3DTextureSize = 256; + + const textureDimensions = Megatexture.get3DTextureDimension( + dimensions, + bytesPerSample, + availableTextureMemoryBytes, + tileCount, + ); + const expectedDimensions = new Cartesian3(92, 224, 182); + expect(textureDimensions).toEqual(expectedDimensions); + const actualMemoryUsage = + textureDimensions.x * + textureDimensions.y * + textureDimensions.z * + bytesPerSample; + expect(actualMemoryUsage).toBeLessThan(availableTextureMemoryBytes); + expect(actualMemoryUsage).toBeGreaterThanOrEqual( + availableTextureMemoryBytes * 0.8, + ); + + ContextLimits._maximum3DTextureSize = maximum3DTextureSize; + }); + + it("3D texture is large enough to fit the number of tiles, but not too much larger", function () { + const dimensions = new Cartesian3(23, 7, 13); + const bytesPerSample = 4; + const tileCount = 8 ** 4; // LODs 0 through 4 fully populated + const availableTextureMemoryBytes = 256 * 1024 * 1024; + + // For consistent test results, temporarily set maximum 3D texture size + // to a value supported on all WebGL2 devices. + const maximum3DTextureSize = ContextLimits.maximum3DTextureSize; + ContextLimits._maximum3DTextureSize = 256; + + const textureDimensions = Megatexture.get3DTextureDimension( + dimensions, + bytesPerSample, + availableTextureMemoryBytes, + tileCount, + ); + const expectedDimensions = new Cartesian3(184, 224, 208); + expect(textureDimensions).toEqual(expectedDimensions); + const maximumTileCount = + (textureDimensions.x / dimensions.x) * + (textureDimensions.y / dimensions.y) * + (textureDimensions.z / dimensions.z); + expect(maximumTileCount).toBeGreaterThanOrEqual(tileCount); + expect(maximumTileCount).toBeLessThanOrEqual(tileCount * 1.25); + + ContextLimits._maximum3DTextureSize = maximum3DTextureSize; + }); + it("adds data to an existing megatexture", function () { const dimension = 16; const dimensions = new Cartesian3(dimension, dimension, dimension); @@ -143,28 +205,6 @@ describe("Scene/Megatexture", function () { }).toThrowDeveloperError(); }); - it("reports approximate memory size", function () { - const tileCount = 4; - const dimension = 16; - const dimensions = new Cartesian3(dimension, dimension, dimension); - const channelCount = 4; - const componentType = MetadataComponentType.FLOAT32; - const textureMemoryByteLength = - tileCount * - dimension ** 3 * - channelCount * - MetadataComponentType.getSizeInBytes(componentType); - - expect( - Megatexture.getApproximateTextureMemoryByteLength( - tileCount, - dimensions, - channelCount, - componentType, - ), - ).toBe(textureMemoryByteLength); - }); - it("destroys", function () { const tileCount = 4; const dimension = 16; diff --git a/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js b/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js index 3c479894f279..f83ca5188543 100644 --- a/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js +++ b/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js @@ -101,7 +101,7 @@ describe( }); expect(primitive.statistics.numberOfTilesWithContentReady).toEqual(1); expect(primitive.statistics.visited).toEqual(1); - expect(primitive.statistics.texturesByteLength).toEqual(67108864); + expect(primitive.statistics.texturesByteLength).toEqual(32); }); it("statistics are updated when constructor option is true", async function () { @@ -174,7 +174,6 @@ describe( }); toggleOption("depthTest", true, false); - toggleOption("nearestSampling", false, true); }); it("sets render parameters", async function () { diff --git a/packages/engine/Specs/Scene/VoxelTraversalSpec.js b/packages/engine/Specs/Scene/VoxelTraversalSpec.js index d57ceb2ff9bc..6b584a6d946b 100644 --- a/packages/engine/Specs/Scene/VoxelTraversalSpec.js +++ b/packages/engine/Specs/Scene/VoxelTraversalSpec.js @@ -145,7 +145,7 @@ describe( }); it("shows texture memory allocation statistic", function () { - expect(traversal.textureMemoryByteLength).toBe(textureMemoryByteLength); + expect(traversal.textureMemoryByteLength).toBe(32); traversal.destroy(); expect(traversal.textureMemoryByteLength).toBe(0); }); @@ -170,9 +170,7 @@ describe( const megatexture = traversal.megatextures[0]; expect(megatexture.occupiedCount).toBe(1); - expect(traversal.textureMemoryByteLength).toEqual( - textureMemoryByteLength, - ); + expect(traversal.textureMemoryByteLength).toEqual(32); }); it("tile failed event is raised", async function () { diff --git a/packages/engine/Specs/Scene/processVoxelPropertiesSpec.js b/packages/engine/Specs/Scene/processVoxelPropertiesSpec.js index 2b30d87f1831..1f424fd21587 100644 --- a/packages/engine/Specs/Scene/processVoxelPropertiesSpec.js +++ b/packages/engine/Specs/Scene/processVoxelPropertiesSpec.js @@ -186,7 +186,7 @@ describe("Scene/processVoxelProperties", function () { ShaderBuilderTester.expectHasFragmentFunctionUnordered( shaderBuilder, "getPropertiesFromMegatextureAtUv", - "Properties getPropertiesFromMegatextureAtUv(vec2 texcoord)", + "Properties getPropertiesFromMegatextureAtUv(vec3 texcoord)", [ " Properties properties;", " properties.a = texture(u_megatextureTextures[0], texcoord).r;", diff --git a/packages/sandcastle/gallery/volumecloud/main.js b/packages/sandcastle/gallery/volumecloud/main.js index acaa108649de..89aa6a3bf0cd 100644 --- a/packages/sandcastle/gallery/volumecloud/main.js +++ b/packages/sandcastle/gallery/volumecloud/main.js @@ -293,9 +293,6 @@ const vertexShader = /* glsl */ ` }`; const fragmentShader = /* glsl */ ` - precision highp float; - precision highp sampler3D; - in vec3 vOrigin; in vec3 vDirection; diff --git a/packages/sandcastle/gallery/volumecloud/sandcastle.yaml b/packages/sandcastle/gallery/volumecloud/sandcastle.yaml index 4d5343b837c9..bf8d983b7f4a 100644 --- a/packages/sandcastle/gallery/volumecloud/sandcastle.yaml +++ b/packages/sandcastle/gallery/volumecloud/sandcastle.yaml @@ -1,6 +1,6 @@ legacyId: VolumeCloud.html title: Volumetric Cloud -description: Render a voumetric cloud with a 3D texture and custom shader code. This example was ported from Three.js's "Volumetric Cloud" example. +description: Render a volumetric cloud with a 3D texture and custom shader code. This example was ported from Three.js's "Volumetric Cloud" example. labels: - Voxels - Graphics diff --git a/packages/sandcastle/gallery/voxels/main.js b/packages/sandcastle/gallery/voxels/main.js index 3b88480f81b1..27e403b8dd8d 100644 --- a/packages/sandcastle/gallery/voxels/main.js +++ b/packages/sandcastle/gallery/voxels/main.js @@ -91,37 +91,7 @@ function ProceduralMultiTileVoxelProvider(shape) { this.types = [Cesium.MetadataType.VEC4]; this.componentTypes = [Cesium.MetadataComponentType.FLOAT32]; this.globalTransform = globalTransform; - this.availableLevels = 2; - this._allVoxelData = new Array(this.availableLevels); - - const allVoxelData = this._allVoxelData; - const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]); - const { dimensions } = this; - - for (let level = 0; level < this.availableLevels; level++) { - const dimAtLevel = Math.pow(2, level); - const voxelCountX = dimensions.x * dimAtLevel; - const voxelCountY = dimensions.y * dimAtLevel; - const voxelCountZ = dimensions.z * dimAtLevel; - const voxelsPerLevel = voxelCountX * voxelCountY * voxelCountZ; - const levelData = (allVoxelData[level] = new Array( - voxelsPerLevel * channelCount, - )); - - for (let z = 0; z < voxelCountX; z++) { - for (let y = 0; y < voxelCountY; y++) { - const indexZY = z * voxelCountY * voxelCountX + y * voxelCountX; - for (let x = 0; x < voxelCountZ; x++) { - const index = (indexZY + x) * channelCount; - levelData[index + 0] = x / (voxelCountX - 1); - levelData[index + 1] = y / (voxelCountY - 1); - levelData[index + 2] = z / (voxelCountZ - 1); - levelData[index + 3] = 0.5; - } - } - } - } } ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { @@ -141,53 +111,43 @@ ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { dimensions.y + paddingBefore.y + paddingAfter.y, dimensions.z + paddingBefore.z + paddingAfter.z, ); - const dimAtLevel = Math.pow(2, tileLevel); - const dimensionsGlobal = Cesium.Cartesian3.fromElements( - dimensions.x * dimAtLevel, - dimensions.y * dimAtLevel, - dimensions.z * dimAtLevel, - ); - const minimumGlobalCoord = Cesium.Cartesian3.ZERO; + const dimAtLevel = 2 ** tileLevel; const maximumGlobalCoord = new Cesium.Cartesian3( - dimensionsGlobal.x - 1, - dimensionsGlobal.y - 1, - dimensionsGlobal.z - 1, + dimensions.x * dimAtLevel - 1, + dimensions.y * dimAtLevel - 1, + dimensions.z * dimAtLevel - 1, ); - let coordGlobal = new Cesium.Cartesian3(); - - const dataGlobal = this._allVoxelData; const dataTile = new Float32Array( paddedDimensions.x * paddedDimensions.y * paddedDimensions.z * channelCount, ); - + const firstSampleCoordinate = Cesium.Cartesian3.fromElements( + tileX * dimensions.x - paddingBefore.x, + tileY * dimensions.y - paddingBefore.y, + tileZ * dimensions.z - paddingBefore.z, + ); + const coordLocal = new Cesium.Cartesian3(); + let coordGlobal = new Cesium.Cartesian3(); for (let z = 0; z < paddedDimensions.z; z++) { const indexZ = z * paddedDimensions.y * paddedDimensions.x; for (let y = 0; y < paddedDimensions.y; y++) { const indexZY = indexZ + y * paddedDimensions.x; for (let x = 0; x < paddedDimensions.x; x++) { const indexTile = indexZY + x; - coordGlobal = Cesium.Cartesian3.clamp( - Cesium.Cartesian3.fromElements( - tileX * dimensions.x + (x - paddingBefore.x), - tileY * dimensions.y + (y - paddingBefore.y), - tileZ * dimensions.z + (z - paddingBefore.z), + Cesium.Cartesian3.add( + firstSampleCoordinate, + Cesium.Cartesian3.fromElements(x, y, z, coordLocal), coordGlobal, ), - minimumGlobalCoord, + Cesium.Cartesian3.ZERO, maximumGlobalCoord, coordGlobal, ); - - const indexGlobal = - coordGlobal.z * dimensionsGlobal.y * dimensionsGlobal.x + - coordGlobal.y * dimensionsGlobal.x + - coordGlobal.x; - - for (let c = 0; c < channelCount; c++) { - dataTile[indexTile * channelCount + c] = - dataGlobal[tileLevel][indexGlobal * channelCount + c]; - } + const index = indexTile * channelCount; + dataTile[index + 0] = coordGlobal.x / maximumGlobalCoord.x; + dataTile[index + 1] = coordGlobal.y / maximumGlobalCoord.y; + dataTile[index + 2] = coordGlobal.z / maximumGlobalCoord.z; + dataTile[index + 3] = 0.75; } } } @@ -220,7 +180,7 @@ const customShaderColor = new Cesium.CustomShader({ float transparency = 1.0 - fsInput.metadata.color.a; // To mimic light scattering, use exponential decay - float thickness = fsInput.voxel.travelDistance * 16.0; + float thickness = fsInput.voxel.travelDistance / 1000000.0; material.alpha = 1.0 - pow(transparency, thickness); }`, });