From 84d81c3a832b278008f7049601be80b4c4bc8d11 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Thu, 24 Jul 2025 14:13:41 -0400 Subject: [PATCH 01/11] Refactors conditional logic of material image loading --- packages/engine/Source/Scene/Material.js | 69 +++++++++++------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 03a0c66c48f1..cfae5a51ca29 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -880,50 +880,43 @@ function createTexture2DUpdateFunction(uniformId) { // they are clonable. That's why we check the url property for Resources // because the instances aren't the same and we keep trying to load the same // image if it fails to load. - const isResource = uniformValue instanceof Resource; + const resource = Resource.createIfNeeded(uniformValue); // Attempt to make a resource. If not already a resource or string, returns original object. if ( - !defined(material._texturePaths[uniformId]) || - (isResource && - uniformValue.url !== material._texturePaths[uniformId].url) || - (!isResource && uniformValue !== material._texturePaths[uniformId]) + resource instanceof Resource && + resource.url !== material._texturePaths[uniformId].url ) { - if (typeof uniformValue === "string" || isResource) { - const resource = isResource - ? uniformValue - : Resource.createIfNeeded(uniformValue); - - let promise; - if (ktx2Regex.test(resource.url)) { - promise = loadKTX2(resource.url); - } else { - promise = resource.fetchImage(); - } + material._texturePaths[uniformId] = uniformValue; + let promise; + if (ktx2Regex.test(resource.url)) { + promise = loadKTX2(resource.url); + } else { + promise = resource.fetchImage(); + } - Promise.resolve(promise) - .then(function (image) { - material._loadedImages.push({ - id: uniformId, - image: image, - }); - }) - .catch(function () { - if (defined(texture) && texture !== material._defaultTexture) { - texture.destroy(); - } - material._textures[uniformId] = material._defaultTexture; + Promise.resolve(promise) + .then(function (image) { + material._loadedImages.push({ + id: uniformId, + image: image, }); - } else if ( - uniformValue instanceof HTMLCanvasElement || + }) + .catch(function () { + if (defined(texture) && texture !== material._defaultTexture) { + texture.destroy(); + } + material._textures[uniformId] = material._defaultTexture; + }); + } else if ( + uniformValue !== material._texturePaths[uniformId] && + (uniformValue instanceof HTMLCanvasElement || uniformValue instanceof HTMLImageElement || uniformValue instanceof ImageBitmap || - uniformValue instanceof OffscreenCanvas - ) { - material._loadedImages.push({ - id: uniformId, - image: uniformValue, - }); - } - + uniformValue instanceof OffscreenCanvas) + ) { + material._loadedImages.push({ + id: uniformId, + image: uniformValue, + }); material._texturePaths[uniformId] = uniformValue; } }; From 2628dfe5c38b76a4038c406998f3bd5c266b1479 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 25 Jul 2025 00:01:15 -0400 Subject: [PATCH 02/11] Refactors material texture 2D image loading into own function. Non-trivial logic reordering, but the core behavior should be the same. Now we can call the image loader on construction, though, and store its promise, which will be useful in the next commit for an async factory constructor. --- packages/engine/Source/Scene/Material.js | 105 ++++++++++++++------ packages/engine/Specs/Scene/MaterialSpec.js | 17 ++-- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index cfae5a51ca29..4d3e0ad610e1 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -327,6 +327,15 @@ function Material(options) { this._defaultTexture = undefined; + /** + * Any and all promises that are created when initializing the material. + * Examples: loading images and cubemaps. + * + * @type {Promise[]} + * @private + */ + this._initializationPromises = []; + initializeMaterial(options, this); Object.defineProperties(this, { type: { @@ -858,10 +867,10 @@ function createTexture2DUpdateFunction(uniformId) { texture.destroy(); } texture = undefined; + material._texturePaths[uniformId] = undefined; } if (!defined(texture)) { - material._texturePaths[uniformId] = undefined; texture = material._textures[uniformId] = material._defaultTexture; uniformDimensionsName = `${uniformId}Dimensions`; @@ -876,52 +885,79 @@ function createTexture2DUpdateFunction(uniformId) { return; } - // When using the entity layer, the Resource objects get recreated on getValue because - // they are clonable. That's why we check the url property for Resources - // because the instances aren't the same and we keep trying to load the same - // image if it fails to load. - const resource = Resource.createIfNeeded(uniformValue); // Attempt to make a resource. If not already a resource or string, returns original object. if ( - resource instanceof Resource && - resource.url !== material._texturePaths[uniformId].url - ) { - material._texturePaths[uniformId] = uniformValue; - let promise; - if (ktx2Regex.test(resource.url)) { - promise = loadKTX2(resource.url); - } else { - promise = resource.fetchImage(); - } - - Promise.resolve(promise) - .then(function (image) { - material._loadedImages.push({ - id: uniformId, - image: image, - }); - }) - .catch(function () { - if (defined(texture) && texture !== material._defaultTexture) { - texture.destroy(); - } - material._textures[uniformId] = material._defaultTexture; - }); - } else if ( - uniformValue !== material._texturePaths[uniformId] && (uniformValue instanceof HTMLCanvasElement || uniformValue instanceof HTMLImageElement || uniformValue instanceof ImageBitmap || - uniformValue instanceof OffscreenCanvas) + uniformValue instanceof OffscreenCanvas) && + uniformValue !== material._texturePaths[uniformId] ) { material._loadedImages.push({ id: uniformId, image: uniformValue, }); material._texturePaths[uniformId] = uniformValue; + return; } + + // If we get to this point, the image should be a string URL or Resource. + // Don't wait on the promise to resolve, just start loading the image and poll status from the update loop. + loadTexture2DImageForUniform(material, uniformId); }; } +function loadTexture2DImageForUniform(material, uniformId) { + const uniforms = material.uniforms; + const uniformValue = uniforms[uniformId]; + if (uniformValue === Material.DefaultImageId) { + return Promise.resolve(); + } + + // Attempt to make a resource from the uniform value. If it's not already a resource or string, this returns the original object. + const resource = Resource.createIfNeeded(uniformValue); + if (!(resource instanceof Resource)) { + return Promise.resolve(); + } + + // When using the entity layer, the Resource objects get recreated on getValue because + // they are clonable. That's why we check the url property for Resources + // because the instances aren't the same and we keep trying to load the same + // image if it fails to load. + const oldResource = Resource.createIfNeeded( + material._texturePaths[uniformId], + ); + const uniformHasChanged = + !defined(oldResource) || oldResource.url !== resource.url; + if (!uniformHasChanged) { + return Promise.resolve(); + } + + let promise; + if (ktx2Regex.test(resource.url)) { + promise = loadKTX2(resource.url); + } else { + promise = resource.fetchImage(); + } + + Promise.resolve(promise) + .then(function (image) { + material._loadedImages.push({ + id: uniformId, + image: image, + }); + }) + .catch(function () { + const texture = material._textures[uniformId]; + if (defined(texture) && texture !== material._defaultTexture) { + texture.destroy(); + } + material._textures[uniformId] = material._defaultTexture; + }); + + material._texturePaths[uniformId] = uniformValue; + return promise; +} + function createCubeMapUpdateFunction(uniformId) { return function (material, context) { const uniformValue = material.uniforms[uniformId]; @@ -1052,6 +1088,9 @@ function createUniform(material, uniformId) { return material._textures[uniformId]; }; material._updateFunctions.push(createTexture2DUpdateFunction(uniformId)); + material._initializationPromises.push( + loadTexture2DImageForUniform(material, uniformId), + ); } else if (uniformType === "samplerCube") { material._uniforms[newUniformId] = function () { return material._textures[uniformId]; diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index 09bed4ff1e01..b5b5fe98f326 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -657,10 +657,12 @@ describe( const ignoreBackground = true; renderMaterial(materialLinear, ignoreBackground); // Populate the scene with the primitive prior to updating + renderMaterial(materialNearest, ignoreBackground); // Populate the scene with the primitive prior to updating return pollToPromise(function () { - const imageLoaded = materialLinear._loadedImages.length !== 0; + const linearImageLoaded = materialLinear._loadedImages.length !== 0; + const nearestImageLoaded = materialNearest._loadedImages.length !== 0; scene.renderForSpecs(); - return imageLoaded; + return linearImageLoaded && nearestImageLoaded; }) .then(function () { renderMaterial(materialLinear, ignoreBackground, function (rgba) { @@ -668,15 +670,8 @@ describe( }); }) .then(function () { - renderMaterial(materialNearest, ignoreBackground); // Populate the scene with the primitive prior to updating - return pollToPromise(function () { - const imageLoaded = materialNearest._loadedImages.length !== 0; - scene.renderForSpecs(); - return imageLoaded; - }).then(function () { - renderMaterial(materialNearest, ignoreBackground, function (rgba) { - expect(rgba).not.toEqualEpsilon(purple, 1); - }); + renderMaterial(materialNearest, ignoreBackground, function (rgba) { + expect(rgba).not.toEqualEpsilon(purple, 1); }); }); }); From 76046ceb4c3227ca61e8cf411f6571862b84f01f Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 25 Jul 2025 00:34:01 -0400 Subject: [PATCH 03/11] Refactors cubemap image loading into own function Behavior ideally stays the same, but now we also call the loader immediately on construction instead of waiting for the Update. This is a prerequisite for an async Material factory constructor method. --- packages/engine/Source/Scene/Material.js | 76 ++++++++++++++---------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 4d3e0ad610e1..43b76a6d8b47 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -973,42 +973,53 @@ function createCubeMapUpdateFunction(uniformId) { } if (!defined(material._textures[uniformId])) { - material._texturePaths[uniformId] = undefined; material._textures[uniformId] = context.defaultCubeMap; } - if (uniformValue === Material.DefaultCubeMapId) { - return; - } + loadCubeMapImagesForUniform(material, uniformId); + }; +} - const path = - uniformValue.positiveX + - uniformValue.negativeX + - uniformValue.positiveY + - uniformValue.negativeY + - uniformValue.positiveZ + - uniformValue.negativeZ; - - if (path !== material._texturePaths[uniformId]) { - const promises = [ - Resource.createIfNeeded(uniformValue.positiveX).fetchImage(), - Resource.createIfNeeded(uniformValue.negativeX).fetchImage(), - Resource.createIfNeeded(uniformValue.positiveY).fetchImage(), - Resource.createIfNeeded(uniformValue.negativeY).fetchImage(), - Resource.createIfNeeded(uniformValue.positiveZ).fetchImage(), - Resource.createIfNeeded(uniformValue.negativeZ).fetchImage(), - ]; - - Promise.all(promises).then(function (images) { - material._loadedCubeMaps.push({ - id: uniformId, - images: images, - }); - }); +async function loadCubeMapImagesForUniform(material, uniformId) { + const uniforms = material.uniforms; + const uniformValue = uniforms[uniformId]; + if (uniformValue === Material.DefaultCubeMapId) { + return Promise.resolve(); + } - material._texturePaths[uniformId] = path; - } - }; + const path = + uniformValue.positiveX + + uniformValue.negativeX + + uniformValue.positiveY + + uniformValue.negativeY + + uniformValue.positiveZ + + uniformValue.negativeZ; + + // The uniform value is unchanged, no update / image load necessary. + if (path === material._texturePaths[uniformId]) { + return Promise.resolve(); + } + + const promises = [ + Resource.createIfNeeded(uniformValue.positiveX).fetchImage(), + Resource.createIfNeeded(uniformValue.negativeX).fetchImage(), + Resource.createIfNeeded(uniformValue.positiveY).fetchImage(), + Resource.createIfNeeded(uniformValue.negativeY).fetchImage(), + Resource.createIfNeeded(uniformValue.positiveZ).fetchImage(), + Resource.createIfNeeded(uniformValue.negativeZ).fetchImage(), + ]; + + const allPromise = Promise.all(promises); + allPromise.then(function (images) { + material._loadedCubeMaps.push({ + id: uniformId, + images: images, + }); + }); + + material._texturePaths[uniformId] = path; + + return allPromise; } function createUniforms(material) { @@ -1096,6 +1107,9 @@ function createUniform(material, uniformId) { return material._textures[uniformId]; }; material._updateFunctions.push(createCubeMapUpdateFunction(uniformId)); + material._initializationPromises.push( + loadCubeMapImagesForUniform(material, uniformId), + ); } else if (uniformType.indexOf("mat") !== -1) { const scratchMatrix = new matrixMap[uniformType](); material._uniforms[newUniformId] = function () { From ecff7ce5de018c8697d864393baa93da286c4990 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 25 Jul 2025 01:19:38 -0400 Subject: [PATCH 04/11] Adds async factory method for Material class --- packages/engine/Source/Scene/Material.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 43b76a6d8b47..7acfe575dd46 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -393,6 +393,36 @@ Material.fromType = function (type, uniforms) { return material; }; +Material.fromTypeAsync = async function (type, uniforms) { + const initializationPromises = []; + // Unlike Material.fromType, we need to specify the uniforms in the Material constructor up front, + // or else anything that needs to be async loaded won't be kicked off until the next Update call. + const material = new Material({ + fabric: { + type: type, + uniforms: uniforms, + }, + }); + + // Recursively collect initialization promises for this material and its submaterials. + getInitializationPromises(material, initializationPromises); + await Promise.all(initializationPromises); + + return material; +}; + +function getInitializationPromises(material, initializationPromises) { + initializationPromises.push(material._initializationPromises); + const submaterials = material.materials; + for (const name in submaterials) { + if (submaterials.hasOwnProperty(name)) { + const submaterial = submaterials[name]; + initializationPromises.push(submaterial._initializationPromises); + getInitializationPromises(submaterial, initializationPromises); + } + } +} + /** * Gets whether or not this material is translucent. * @returns {boolean} true if this material is translucent, false otherwise. From aa8d7c25b3215d483436e6c91286530990490fde Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 25 Jul 2025 10:13:38 -0400 Subject: [PATCH 05/11] Adds jsdoc for new Material methods in async refactor --- packages/engine/Source/Scene/Material.js | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 7acfe575dd46..d0e63ea3ac9e 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -393,7 +393,28 @@ Material.fromType = function (type, uniforms) { return material; }; +/** + * Creates a new material using an existing material type and returns a promise that resolves when + * all of the material's resources have been loaded. + * + * @param {string} type The base material type. + * @param {object} [uniforms] Overrides for the default uniforms. + * @returns {Promise} A promise that resolves to a new material object when all resources are loaded. + * + * @exception {DeveloperError} material with that type does not exist. + * + * @example + * const material = await Cesium.Material.fromTypeAsync('Image', { + * image: '../Images/Cesium_Logo_overlay.png' + * }); + */ Material.fromTypeAsync = async function (type, uniforms) { + //>>includeStart('debug', pragmas.debug); + if (!defined(Material._materialCache.getMaterial(type))) { + throw new DeveloperError(`material with type '${type}' does not exist.`); + } + //>>includeEnd('debug'); + const initializationPromises = []; // Unlike Material.fromType, we need to specify the uniforms in the Material constructor up front, // or else anything that needs to be async loaded won't be kicked off until the next Update call. @@ -411,6 +432,13 @@ Material.fromTypeAsync = async function (type, uniforms) { return material; }; +/** + * Recursively traverses the material and its submaterials to collect all initialization promises. + * @param {Material} material The material to traverse. + * @param {Promise[]} initializationPromises The array to collect promises into. + * + * @private + */ function getInitializationPromises(material, initializationPromises) { initializationPromises.push(material._initializationPromises); const submaterials = material.materials; @@ -936,6 +964,16 @@ function createTexture2DUpdateFunction(uniformId) { }; } +/** + * For a given uniform ID, potentially loads a texture image for the material, if the uniform value is a Resource or string URL, + * and has changed since the last time this was called (either on construction or update). + * + * @param {Material} material The material to load the texture for. + * @param {string} uniformId The ID of the uniform of the image. + * @returns {Promise} A promise that resolves when the image is loaded, or a resolved promise if image loading is not necessary. + * + * @private + */ function loadTexture2DImageForUniform(material, uniformId) { const uniforms = material.uniforms; const uniformValue = uniforms[uniformId]; @@ -1010,6 +1048,13 @@ function createCubeMapUpdateFunction(uniformId) { }; } +/** + * Loads the images for a cubemap uniform, if it has changed since the last time this was called. + * + * @param {Material} material The material to load the cubemap images for. + * @param {string} uniformId The ID of the uniform that corresponds to the cubemap images. + * @returns {Promise} A promise that resolves when the images are loaded, or a resolved promise if image loading is not necessary. + */ async function loadCubeMapImagesForUniform(material, uniformId) { const uniforms = material.uniforms; const uniformValue = uniforms[uniformId]; From 59161b0d50cb5339f0f9288ba3108a425c2da69d Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 25 Jul 2025 13:13:57 -0400 Subject: [PATCH 06/11] Adds unit tests for async material constructor --- packages/engine/Source/Scene/Material.js | 18 ++--- packages/engine/Specs/Scene/MaterialSpec.js | 87 +++++++++++++++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index d0e63ea3ac9e..9046b255c51e 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -440,12 +440,11 @@ Material.fromTypeAsync = async function (type, uniforms) { * @private */ function getInitializationPromises(material, initializationPromises) { - initializationPromises.push(material._initializationPromises); + initializationPromises.push(...material._initializationPromises); const submaterials = material.materials; for (const name in submaterials) { if (submaterials.hasOwnProperty(name)) { const submaterial = submaterials[name]; - initializationPromises.push(submaterial._initializationPromises); getInitializationPromises(submaterial, initializationPromises); } } @@ -653,6 +652,7 @@ function initializeMaterial(options, result) { result._strict = options.strict ?? false; result._count = options.count ?? 0; result._template = clone(options.fabric ?? Frozen.EMPTY_OBJECT); + result.fabric = clone(options.fabric ?? Frozen.EMPTY_OBJECT); result._template.uniforms = clone( result._template.uniforms ?? Frozen.EMPTY_OBJECT, ); @@ -683,15 +683,15 @@ function initializeMaterial(options, result) { // Make sure the template has no obvious errors. More error checking happens later. checkForTemplateErrors(result); + createMethodDefinition(result); + createUniforms(result); + createSubMaterials(result); + // If the material has a new type, add it to the cache. if (!defined(cachedMaterial)) { Material._materialCache.addMaterial(result.type, result); } - createMethodDefinition(result); - createUniforms(result); - createSubMaterials(result); - const defaultTranslucent = result._translucentFunctions.length === 0 ? true : undefined; translucent = translucent ?? defaultTranslucent; @@ -970,7 +970,7 @@ function createTexture2DUpdateFunction(uniformId) { * * @param {Material} material The material to load the texture for. * @param {string} uniformId The ID of the uniform of the image. - * @returns {Promise} A promise that resolves when the image is loaded, or a resolved promise if image loading is not necessary. + * @returns A promise that resolves when the image is loaded, or a resolved promise if image loading is not necessary. * * @private */ @@ -1053,9 +1053,9 @@ function createCubeMapUpdateFunction(uniformId) { * * @param {Material} material The material to load the cubemap images for. * @param {string} uniformId The ID of the uniform that corresponds to the cubemap images. - * @returns {Promise} A promise that resolves when the images are loaded, or a resolved promise if image loading is not necessary. + * @returns A promise that resolves when the images are loaded, or a resolved promise if image loading is not necessary. */ -async function loadCubeMapImagesForUniform(material, uniformId) { +function loadCubeMapImagesForUniform(material, uniformId) { const uniforms = material.uniforms; const uniformValue = uniforms[uniformId]; if (uniformValue === Material.DefaultCubeMapId) { diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index b5b5fe98f326..72ba7b4e4833 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -630,6 +630,93 @@ describe( }); }); + it("creates a material using fromTypeAsync", async function () { + const material = await Material.fromTypeAsync("Color"); + renderMaterial(material); + }); + + it("loads a 2D texture image synchronously when awaiting fromTypeAsync", async function () { + const imageMaterial = await Material.fromTypeAsync("Image", { + image: "./Data/Images/Blue.png", + }); + renderMaterial(imageMaterial); + }); + + it("loads cubemap images synchronously when awaiting fromTypeAsync", async function () { + // First make a material with a cubemap, then use its type to make a second cubemap material asynchronously. + const material = new Material({ + strict: true, + fabric: { + uniforms: { + cubeMap: { + positiveX: "./Data/Images/Blue.png", + negativeX: "./Data/Images/Blue.png", + positiveY: "./Data/Images/Blue.png", + negativeY: "./Data/Images/Blue.png", + positiveZ: "./Data/Images/Blue.png", + negativeZ: "./Data/Images/Blue.png", + }, + }, + source: + "uniform samplerCube cubeMap;\n" + + "czm_material czm_getMaterial(czm_materialInput materialInput)\n" + + "{\n" + + " czm_material material = czm_getDefaultMaterial(materialInput);\n" + + " material.diffuse = czm_textureCube(cubeMap, vec3(1.0)).xyz;\n" + + " return material;\n" + + "}\n", + }, + }); + + const materialFromTypeAsync = await Material.fromTypeAsync( + material.type, + { + cubeMap: { + positiveX: "./Data/Images/Green.png", + negativeX: "./Data/Images/Green.png", + positiveY: "./Data/Images/Green.png", + negativeY: "./Data/Images/Green.png", + positiveZ: "./Data/Images/Green.png", + negativeZ: "./Data/Images/Green.png", + }, + }, + ); + + renderMaterial(materialFromTypeAsync); + }); + + it("loads sub-materials synchronously when awaiting fromTypeAsync", async function () { + // First make a material with submaterials, then use its type to make a second material asynchronously. + const material = new Material({ + strict: true, + fabric: { + materials: { + greenMaterial: { + type: "Image", + uniforms: { + image: "./Data/Images/Green.png", // Green image + }, + }, + blueMaterial: { + type: "Image", + uniforms: { + image: "./Data/Images/Blue.png", // Blue image + }, + }, + }, + components: { + diffuse: + "clamp(greenMaterial.diffuse + blueMaterial.diffuse, 0.0, 1.0)", + }, + }, + }); + + const materialFromTypeAsync = await Material.fromTypeAsync(material.type); + renderMaterial(materialFromTypeAsync, false, function (rgba) { + expect(rgba).toEqual([0, 255, 255, 255]); // Expect cyan from green + blue + }); + }); + it("creates material with custom texture filter", function () { const materialLinear = new Material({ fabric: { From 781c7bac911250673efbbc1f0bf058488732a1c6 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Mon, 28 Jul 2025 12:17:34 -0400 Subject: [PATCH 07/11] Material unit test update and CHANGES.md --- CHANGES.md | 12 +++++ packages/engine/Specs/Scene/MaterialSpec.js | 52 ++++++--------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 31c32983fc27..956b78e3028d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Change Log +## 1.133 - 2025-09-01 + +### @cesium/engine + +#### Fixes :wrench: + +- Materials loaded from type now respect submaterials present in the referenced material type. [#10566](https://github.com/CesiumGS/cesium/issues/10566) + +#### Additions :tada: + +- Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566) + ## 1.132 - 2025-08-01 ### @cesium/engine diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index 72ba7b4e4833..24b7af9d746d 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -717,50 +717,28 @@ describe( }); }); - it("creates material with custom texture filter", function () { - const materialLinear = new Material({ - fabric: { - type: "DiffuseMap", - uniforms: { - image: "./Data/Images/BlueOverRed.png", - }, - }, - minificationFilter: TextureMinificationFilter.LINEAR, - magnificationFilter: TextureMagnificationFilter.LINEAR, + it("creates material with custom texture filter", async function () { + const materialLinear = await Material.fromTypeAsync("DiffuseMap", { + image: "./Data/Images/BlueOverRed.png", }); + materialLinear._minificationFilter = TextureMinificationFilter.LINEAR; + materialLinear._magnificationFilter = TextureMagnificationFilter.LINEAR; - const materialNearest = new Material({ - fabric: { - type: "DiffuseMap", - uniforms: { - image: "./Data/Images/BlueOverRed.png", - }, - }, - minificationFilter: TextureMinificationFilter.NEAREST, - magnificationFilter: TextureMagnificationFilter.NEAREST, + const materialNearest = await Material.fromTypeAsync("DiffuseMap", { + image: "./Data/Images/BlueOverRed.png", }); + materialNearest._minificationFilter = TextureMinificationFilter.NEAREST; + materialNearest._magnificationFilter = TextureMagnificationFilter.NEAREST; const purple = [127, 0, 127, 255]; const ignoreBackground = true; - renderMaterial(materialLinear, ignoreBackground); // Populate the scene with the primitive prior to updating - renderMaterial(materialNearest, ignoreBackground); // Populate the scene with the primitive prior to updating - return pollToPromise(function () { - const linearImageLoaded = materialLinear._loadedImages.length !== 0; - const nearestImageLoaded = materialNearest._loadedImages.length !== 0; - scene.renderForSpecs(); - return linearImageLoaded && nearestImageLoaded; - }) - .then(function () { - renderMaterial(materialLinear, ignoreBackground, function (rgba) { - expect(rgba).toEqualEpsilon(purple, 1); - }); - }) - .then(function () { - renderMaterial(materialNearest, ignoreBackground, function (rgba) { - expect(rgba).not.toEqualEpsilon(purple, 1); - }); - }); + renderMaterial(materialLinear, ignoreBackground, function (rgba) { + expect(rgba).toEqualEpsilon(purple, 1); + }); + renderMaterial(materialNearest, ignoreBackground, function (rgba) { + expect(rgba).not.toEqualEpsilon(purple, 1); + }); }); it("handles when material image is undefined", function () { From 7b12d896bc6a78d44ac851aad3c565e7e2fe6089 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Tue, 29 Jul 2025 11:47:17 -0400 Subject: [PATCH 08/11] Adds Material constructor options to fromType and fromTypeAsync methods --- packages/engine/Source/Scene/Material.js | 18 +++++++++-- packages/engine/Specs/Scene/MaterialSpec.js | 34 ++++++++++++++------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 9046b255c51e..1adb22e69e5c 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -360,6 +360,12 @@ Material._uniformList = {}; * * @param {string} type The base material type. * @param {object} [uniforms] Overrides for the default uniforms. + * @param {object} [options] Object with the following properties: + * @param {boolean} [options.strict=false] Throws errors for issues that would normally be ignored, including unused uniforms or materials. + * @param {boolean|Function} [options.translucent=true] When true or a function that returns true, the geometry + * with this material is expected to appear translucent. + * @param {TextureMinificationFilter} [options.minificationFilter=TextureMinificationFilter.LINEAR] The {@link TextureMinificationFilter} to apply to this material's textures. + * @param {TextureMagnificationFilter} [options.magnificationFilter=TextureMagnificationFilter.LINEAR] The {@link TextureMagnificationFilter} to apply to this material's textures. * @returns {Material} New material object. * * @exception {DeveloperError} material with that type does not exist. @@ -369,7 +375,7 @@ Material._uniformList = {}; * color: new Cesium.Color(1.0, 0.0, 0.0, 1.0) * }); */ -Material.fromType = function (type, uniforms) { +Material.fromType = function (type, uniforms, options) { //>>includeStart('debug', pragmas.debug); if (!defined(Material._materialCache.getMaterial(type))) { throw new DeveloperError(`material with type '${type}' does not exist.`); @@ -377,6 +383,7 @@ Material.fromType = function (type, uniforms) { //>>includeEnd('debug'); const material = new Material({ + ...options, fabric: { type: type, }, @@ -399,6 +406,12 @@ Material.fromType = function (type, uniforms) { * * @param {string} type The base material type. * @param {object} [uniforms] Overrides for the default uniforms. + * @param {object} [options] Object with the following properties: + * @param {boolean} [options.strict=false] Throws errors for issues that would normally be ignored, including unused uniforms or materials. + * @param {boolean|Function} [options.translucent=true] When true or a function that returns true, the geometry + * with this material is expected to appear translucent. + * @param {TextureMinificationFilter} [options.minificationFilter=TextureMinificationFilter.LINEAR] The {@link TextureMinificationFilter} to apply to this material's textures. + * @param {TextureMagnificationFilter} [options.magnificationFilter=TextureMagnificationFilter.LINEAR] The {@link TextureMagnificationFilter} to apply to this material's textures. * @returns {Promise} A promise that resolves to a new material object when all resources are loaded. * * @exception {DeveloperError} material with that type does not exist. @@ -408,7 +421,7 @@ Material.fromType = function (type, uniforms) { * image: '../Images/Cesium_Logo_overlay.png' * }); */ -Material.fromTypeAsync = async function (type, uniforms) { +Material.fromTypeAsync = async function (type, uniforms, options) { //>>includeStart('debug', pragmas.debug); if (!defined(Material._materialCache.getMaterial(type))) { throw new DeveloperError(`material with type '${type}' does not exist.`); @@ -419,6 +432,7 @@ Material.fromTypeAsync = async function (type, uniforms) { // Unlike Material.fromType, we need to specify the uniforms in the Material constructor up front, // or else anything that needs to be async loaded won't be kicked off until the next Update call. const material = new Material({ + ...options, fabric: { type: type, uniforms: uniforms, diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index 24b7af9d746d..b64544ca1ee7 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -639,7 +639,9 @@ describe( const imageMaterial = await Material.fromTypeAsync("Image", { image: "./Data/Images/Blue.png", }); - renderMaterial(imageMaterial); + renderMaterial(imageMaterial, false, function (rgba) { + expect(rgba).toEqual([0, 0, 255, 255]); + }); }); it("loads cubemap images synchronously when awaiting fromTypeAsync", async function () { @@ -718,17 +720,27 @@ describe( }); it("creates material with custom texture filter", async function () { - const materialLinear = await Material.fromTypeAsync("DiffuseMap", { - image: "./Data/Images/BlueOverRed.png", - }); - materialLinear._minificationFilter = TextureMinificationFilter.LINEAR; - materialLinear._magnificationFilter = TextureMagnificationFilter.LINEAR; + const materialLinear = await Material.fromTypeAsync( + "DiffuseMap", + { + image: "./Data/Images/BlueOverRed.png", + }, + { + minificationFilter: TextureMinificationFilter.LINEAR, + magnificationFilter: TextureMagnificationFilter.LINEAR, + }, + ); - const materialNearest = await Material.fromTypeAsync("DiffuseMap", { - image: "./Data/Images/BlueOverRed.png", - }); - materialNearest._minificationFilter = TextureMinificationFilter.NEAREST; - materialNearest._magnificationFilter = TextureMagnificationFilter.NEAREST; + const materialNearest = await Material.fromTypeAsync( + "DiffuseMap", + { + image: "./Data/Images/BlueOverRed.png", + }, + { + minificationFilter: TextureMinificationFilter.NEAREST, + magnificationFilter: TextureMagnificationFilter.NEAREST, + }, + ); const purple = [127, 0, 127, 255]; From bf43372392a4cb1ddcfac747a40ea0ec74e8e519 Mon Sep 17 00:00:00 2001 From: ggetz Date: Mon, 4 Aug 2025 14:51:29 -0400 Subject: [PATCH 09/11] Some minor test cleanup --- packages/engine/Specs/Scene/MaterialSpec.js | 169 ++++++++------------ 1 file changed, 65 insertions(+), 104 deletions(-) diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index b64544ca1ee7..e7bcb001d180 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -24,7 +24,6 @@ describe( function () { let scene; - const rectangle = Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0); let polygon; const backgroundColor = [0, 0, 128, 255]; let polylines; @@ -40,7 +39,9 @@ describe( scene.backgroundColor, ); scene.primitives.destroyPrimitives = false; - scene.camera.setView({ destination: rectangle }); + scene.camera.setView({ + destination: Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0), + }); }); afterAll(function () { @@ -54,7 +55,7 @@ describe( geometryInstances: new GeometryInstance({ geometry: new RectangleGeometry({ vertexFormat: vertexFormat, - rectangle: rectangle, + rectangle: Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0), }), }), asynchronous: false, @@ -82,6 +83,19 @@ describe( polylines = polylines && polylines.destroy(); }); + function itRenders(initialColor = backgroundColor) { + it("renders", function () { + expect(scene).toRender(initialColor); + + scene.primitives.removeAll(); + scene.primitives.add(polygon); + + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba).not.toEqual(backgroundColor); + }); + }); + } + function renderMaterial(material, ignoreBackground, callback) { ignoreBackground = ignoreBackground ?? false; polygon.appearance.material = material; @@ -100,116 +114,63 @@ describe( }); } - function renderPolylineMaterial(material) { - polyline.material = material; - expect(scene).toRender(backgroundColor); - - scene.primitives.removeAll(); - scene.primitives.add(polylines); - - let result; - expect(scene).toRenderAndCall(function (rgba) { - result = rgba; - expect(rgba).not.toEqual(backgroundColor); - }); - return result; - } - function verifyMaterial(type) { - const material = new Material({ - strict: true, - fabric: { - type: type, - }, - }); - renderMaterial(material); - } + describe(`${type} built-in material`, function () { + beforeEach(function () { + const material = new Material({ + strict: true, + fabric: { + type: type, + }, + }); + polygon.appearance.material = material; + }); - function verifyPolylineMaterial(type) { - const material = new Material({ - strict: true, - fabric: { - type: type, - }, + itRenders(); }); - renderPolylineMaterial(material); } - it("draws Color built-in material", function () { - verifyMaterial("Color"); - }); - - it("draws Image built-in material", function () { - verifyMaterial("Image"); - }); - - it("draws DiffuseMap built-in material", function () { - verifyMaterial("DiffuseMap"); - }); - - it("draws AlphaMap built-in material", function () { - verifyMaterial("AlphaMap"); - }); - - it("draws SpecularMap built-in material", function () { - verifyMaterial("SpecularMap"); - }); - - it("draws EmissionMap built-in material", function () { - verifyMaterial("EmissionMap"); - }); - - it("draws BumpMap built-in material", function () { - verifyMaterial("BumpMap"); - }); - - it("draws NormalMap built-in material", function () { - verifyMaterial("NormalMap"); - }); - - it("draws Grid built-in material", function () { - verifyMaterial("Grid"); - }); - - it("draws Stripe built-in material", function () { - verifyMaterial("Stripe"); - }); - - it("draws Checkerboard built-in material", function () { - verifyMaterial("Checkerboard"); - }); - - it("draws Dot built-in material", function () { - verifyMaterial("Dot"); - }); - - it("draws Water built-in material", function () { - verifyMaterial("Water"); - }); - - it("draws RimLighting built-in material", function () { - verifyMaterial("RimLighting"); - }); - - it("draws Fade built-in material", function () { - verifyMaterial("Fade"); - }); + function verifyPolylineMaterial(type) { + describe(`${type} built-in material`, function () { + it("renders", function () { + const material = new Material({ + strict: true, + fabric: { + type: type, + }, + }); - it("draws PolylineArrow built-in material", function () { - verifyPolylineMaterial("PolylineArrow"); - }); + polyline.material = material; + expect(scene).toRender(backgroundColor); - it("draws PolylineDash built-in material", function () { - verifyPolylineMaterial("PolylineDash"); - }); + scene.primitives.removeAll(); + scene.primitives.add(polylines); - it("draws PolylineGlow built-in material", function () { - verifyPolylineMaterial("PolylineGlow"); - }); + expect(scene).notToRender(backgroundColor); + }); + }); + } - it("draws PolylineOutline built-in material", function () { - verifyPolylineMaterial("PolylineOutline"); - }); + verifyMaterial("Color"); + verifyMaterial("Image"); + verifyMaterial("DiffuseMap"); + verifyMaterial("AlphaMap"); + verifyMaterial("SpecularMap"); + verifyMaterial("EmissionMap"); + verifyMaterial("BumpMap"); + verifyMaterial("NormalMap"); + verifyMaterial("Grid"); + verifyMaterial("Stripe"); + verifyMaterial("Checkerboard"); + verifyMaterial("Dot"); + verifyMaterial("Water"); + verifyMaterial("RimLighting"); + verifyMaterial("Fade"); + + verifyPolylineMaterial("PolylineArrow"); + verifyPolylineMaterial("PolylineDash"); + verifyPolylineMaterial("PolylineGlow"); + verifyPolylineMaterial("PolylineOutline"); it("gets the material type", function () { const material = new Material({ From 39672a6f8441156a43b6ddb2db7e90a3c81a6803 Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 8 Aug 2025 14:12:42 -0400 Subject: [PATCH 10/11] Rethrows errors from Material async factory method --- packages/engine/Source/Scene/Material.js | 30 ++++++++++++++++----- packages/engine/Specs/Scene/MaterialSpec.js | 13 +++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index 1adb22e69e5c..a8b981690dc1 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -336,6 +336,15 @@ function Material(options) { */ this._initializationPromises = []; + /** + * An error that occurred in async operations during material initialization. + * Only one error is stored. + * + * @type {Error|undefined} + * @private + */ + this._initializationError = undefined; + initializeMaterial(options, this); Object.defineProperties(this, { type: { @@ -443,6 +452,10 @@ Material.fromTypeAsync = async function (type, uniforms, options) { getInitializationPromises(material, initializationPromises); await Promise.all(initializationPromises); + if (defined(material._initializationError)) { + throw material._initializationError; + } + return material; }; @@ -1028,7 +1041,8 @@ function loadTexture2DImageForUniform(material, uniformId) { image: image, }); }) - .catch(function () { + .catch(function (error) { + material._initializationError = error; const texture = material._textures[uniformId]; if (defined(texture) && texture !== material._defaultTexture) { texture.destroy(); @@ -1099,12 +1113,16 @@ function loadCubeMapImagesForUniform(material, uniformId) { ]; const allPromise = Promise.all(promises); - allPromise.then(function (images) { - material._loadedCubeMaps.push({ - id: uniformId, - images: images, + allPromise + .then(function (images) { + material._loadedCubeMaps.push({ + id: uniformId, + images: images, + }); + }) + .catch(function (error) { + material._initializationError = error; }); - }); material._texturePaths[uniformId] = path; diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index e7bcb001d180..549b3f9a64b7 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -14,6 +14,7 @@ import { Primitive, TextureMagnificationFilter, TextureMinificationFilter, + DeveloperError, } from "../../index.js"; import createScene from "../../../../Specs/createScene.js"; @@ -1118,6 +1119,18 @@ describe( renderMaterial(material); material.destroy(); }); + + it("throws when loaded async and image loading fails", async function () { + spyOn(Resource.prototype, "fetchImage").and.callFake(function () { + return Promise.reject(new DeveloperError("Image loading failed")); + }); + + await expectAsync( + Material.fromTypeAsync("DiffuseMap", { + image: "i_dont_exist.png", + }), + ).toBeRejectedWithDeveloperError("Image loading failed"); + }); }, "WebGL", ); From 360fa59b504ea42c29b7b9de43194f23b4518aac Mon Sep 17 00:00:00 2001 From: Matt Schwartz Date: Fri, 8 Aug 2025 14:50:03 -0400 Subject: [PATCH 11/11] Removes Material fromType options in favor of setters --- packages/engine/Source/Scene/Material.js | 47 ++++++++++++++------- packages/engine/Specs/Scene/MaterialSpec.js | 30 +++++-------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/engine/Source/Scene/Material.js b/packages/engine/Source/Scene/Material.js index a8b981690dc1..90a441ab3e35 100644 --- a/packages/engine/Source/Scene/Material.js +++ b/packages/engine/Source/Scene/Material.js @@ -351,6 +351,34 @@ function Material(options) { value: this.type, writable: false, }, + + /** + * The {@link TextureMinificationFilter} to apply to this material's textures. + * @type {TextureMinificationFilter} + * @default TextureMinificationFilter.LINEAR + */ + minificationFilter: { + get: function () { + return this._minificationFilter; + }, + set: function (value) { + this._minificationFilter = value; + }, + }, + + /** + * The {@link TextureMagnificationFilter} to apply to this material's textures. + * @type {TextureMagnificationFilter} + * @default TextureMagnificationFilter.LINEAR + */ + magnificationFilter: { + get: function () { + return this._magnificationFilter; + }, + set: function (value) { + this._magnificationFilter = value; + }, + }, }); if (!defined(Material._uniformList[this.type])) { @@ -369,12 +397,6 @@ Material._uniformList = {}; * * @param {string} type The base material type. * @param {object} [uniforms] Overrides for the default uniforms. - * @param {object} [options] Object with the following properties: - * @param {boolean} [options.strict=false] Throws errors for issues that would normally be ignored, including unused uniforms or materials. - * @param {boolean|Function} [options.translucent=true] When true or a function that returns true, the geometry - * with this material is expected to appear translucent. - * @param {TextureMinificationFilter} [options.minificationFilter=TextureMinificationFilter.LINEAR] The {@link TextureMinificationFilter} to apply to this material's textures. - * @param {TextureMagnificationFilter} [options.magnificationFilter=TextureMagnificationFilter.LINEAR] The {@link TextureMagnificationFilter} to apply to this material's textures. * @returns {Material} New material object. * * @exception {DeveloperError} material with that type does not exist. @@ -384,7 +406,7 @@ Material._uniformList = {}; * color: new Cesium.Color(1.0, 0.0, 0.0, 1.0) * }); */ -Material.fromType = function (type, uniforms, options) { +Material.fromType = function (type, uniforms) { //>>includeStart('debug', pragmas.debug); if (!defined(Material._materialCache.getMaterial(type))) { throw new DeveloperError(`material with type '${type}' does not exist.`); @@ -392,7 +414,6 @@ Material.fromType = function (type, uniforms, options) { //>>includeEnd('debug'); const material = new Material({ - ...options, fabric: { type: type, }, @@ -415,12 +436,6 @@ Material.fromType = function (type, uniforms, options) { * * @param {string} type The base material type. * @param {object} [uniforms] Overrides for the default uniforms. - * @param {object} [options] Object with the following properties: - * @param {boolean} [options.strict=false] Throws errors for issues that would normally be ignored, including unused uniforms or materials. - * @param {boolean|Function} [options.translucent=true] When true or a function that returns true, the geometry - * with this material is expected to appear translucent. - * @param {TextureMinificationFilter} [options.minificationFilter=TextureMinificationFilter.LINEAR] The {@link TextureMinificationFilter} to apply to this material's textures. - * @param {TextureMagnificationFilter} [options.magnificationFilter=TextureMagnificationFilter.LINEAR] The {@link TextureMagnificationFilter} to apply to this material's textures. * @returns {Promise} A promise that resolves to a new material object when all resources are loaded. * * @exception {DeveloperError} material with that type does not exist. @@ -430,7 +445,7 @@ Material.fromType = function (type, uniforms, options) { * image: '../Images/Cesium_Logo_overlay.png' * }); */ -Material.fromTypeAsync = async function (type, uniforms, options) { +Material.fromTypeAsync = async function (type, uniforms) { //>>includeStart('debug', pragmas.debug); if (!defined(Material._materialCache.getMaterial(type))) { throw new DeveloperError(`material with type '${type}' does not exist.`); @@ -441,7 +456,6 @@ Material.fromTypeAsync = async function (type, uniforms, options) { // Unlike Material.fromType, we need to specify the uniforms in the Material constructor up front, // or else anything that needs to be async loaded won't be kicked off until the next Update call. const material = new Material({ - ...options, fabric: { type: type, uniforms: uniforms, @@ -451,6 +465,7 @@ Material.fromTypeAsync = async function (type, uniforms, options) { // Recursively collect initialization promises for this material and its submaterials. getInitializationPromises(material, initializationPromises); await Promise.all(initializationPromises); + initializationPromises.length = 0; if (defined(material._initializationError)) { throw material._initializationError; diff --git a/packages/engine/Specs/Scene/MaterialSpec.js b/packages/engine/Specs/Scene/MaterialSpec.js index 549b3f9a64b7..17eddd773197 100644 --- a/packages/engine/Specs/Scene/MaterialSpec.js +++ b/packages/engine/Specs/Scene/MaterialSpec.js @@ -682,27 +682,17 @@ describe( }); it("creates material with custom texture filter", async function () { - const materialLinear = await Material.fromTypeAsync( - "DiffuseMap", - { - image: "./Data/Images/BlueOverRed.png", - }, - { - minificationFilter: TextureMinificationFilter.LINEAR, - magnificationFilter: TextureMagnificationFilter.LINEAR, - }, - ); + const materialLinear = await Material.fromTypeAsync("DiffuseMap", { + image: "./Data/Images/BlueOverRed.png", + }); + materialLinear.minificationFilter = TextureMinificationFilter.LINEAR; + materialLinear.magnificationFilter = TextureMagnificationFilter.LINEAR; - const materialNearest = await Material.fromTypeAsync( - "DiffuseMap", - { - image: "./Data/Images/BlueOverRed.png", - }, - { - minificationFilter: TextureMinificationFilter.NEAREST, - magnificationFilter: TextureMagnificationFilter.NEAREST, - }, - ); + const materialNearest = await Material.fromTypeAsync("DiffuseMap", { + image: "./Data/Images/BlueOverRed.png", + }); + materialNearest.minificationFilter = TextureMinificationFilter.NEAREST; + materialNearest.magnificationFilter = TextureMagnificationFilter.NEAREST; const purple = [127, 0, 127, 255];