diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 42110f3c3bc5..01141c16d599 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -3998,7 +3998,7 @@ export abstract class GltfReader { protected readonly _deduplicateVertices: boolean; defaultWrapMode: GltfWrapMode; // (undocumented) - protected findTextureMapping(id: string | undefined, isTransparent: boolean, normalMapId: string | undefined): TextureMapping | undefined; + protected findTextureMapping(id: string | undefined, isTransparent: boolean, normalMapId: string | undefined, constantLodParamProps: TextureMapping.ConstantLodParamProps | undefined, normalMapUseConstantLod?: boolean): TextureMapping | undefined; // (undocumented) getBufferView(json: { [k: string]: any; @@ -7042,6 +7042,8 @@ export interface MeshArgs { textureMapping?: { texture: RenderTexture; uvParams: Point2d[]; + useConstantLod?: boolean; + constantLodParams?: TextureMapping.ConstantLodParamProps; }; vertIndices: number[]; } diff --git a/common/changes/@itwin/core-common/eringram-constant-lod-gltf_2026-01-09-17-16.json b/common/changes/@itwin/core-common/eringram-constant-lod-gltf_2026-01-09-17-16.json new file mode 100644 index 000000000000..d1ac065f5d72 --- /dev/null +++ b/common/changes/@itwin/core-common/eringram-constant-lod-gltf_2026-01-09-17-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-frontend/eringram-constant-lod-gltf_2026-01-09-17-16.json b/common/changes/@itwin/core-frontend/eringram-constant-lod-gltf_2026-01-09-17-16.json new file mode 100644 index 000000000000..63004d50c9c4 --- /dev/null +++ b/common/changes/@itwin/core-frontend/eringram-constant-lod-gltf_2026-01-09-17-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-frontend", + "comment": "Support EXT_textureInfo_constant_lod glTF extension", + "type": "none" + } + ], + "packageName": "@itwin/core-frontend" +} \ No newline at end of file diff --git a/core/common/src/test/TextureMapping.test.ts b/core/common/src/test/TextureMapping.test.ts new file mode 100644 index 000000000000..70e688df6d4c --- /dev/null +++ b/core/common/src/test/TextureMapping.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +import { describe, expect, it } from "vitest"; +import { TextureMapping } from "../TextureMapping"; + +describe("TextureMapping.Params", () => { + describe("constructor", () => { + it("applies default values when no props provided", () => { + const params = new TextureMapping.Params(); + + expect(params.mode).toBe(TextureMapping.Mode.Parametric); + expect(params.weight).toBe(1); + expect(params.worldMapping).toBe(false); + expect(params.useConstantLod).toBe(false); + expect(params.textureMatrix).toBe(TextureMapping.Trans2x3.identity); + + // Constant LOD params should have defaults + expect(params.constantLodParams.repetitions).toBe(1); + expect(params.constantLodParams.offset).toEqual({ x: 0, y: 0 }); + expect(params.constantLodParams.minDistClamp).toBe(1); + expect(params.constantLodParams.maxDistClamp).toBe(4096 * 1024 * 1024); + }); + + it("applies default constant LOD values when useConstantLod is true but no constantLodProps provided", () => { + const params = new TextureMapping.Params({ useConstantLod: true }); + + expect(params.useConstantLod).toBe(true); + expect(params.constantLodParams.repetitions).toBe(1); + expect(params.constantLodParams.offset).toEqual({ x: 0, y: 0 }); + expect(params.constantLodParams.minDistClamp).toBe(1); + expect(params.constantLodParams.maxDistClamp).toBe(4096 * 1024 * 1024); + }); + + it("applies default constant LOD values for missing properties in constantLodProps", () => { + // Only provide repetitions, other props should use defaults + const params = new TextureMapping.Params({ + useConstantLod: true, + constantLodProps: { repetitions: 5.0 }, + }); + + expect(params.constantLodParams.repetitions).toBe(5.0); + expect(params.constantLodParams.offset).toEqual({ x: 0, y: 0 }); + expect(params.constantLodParams.minDistClamp).toBe(1); + expect(params.constantLodParams.maxDistClamp).toBe(4096 * 1024 * 1024); + }); + + it("uses provided constant LOD values when all are specified", () => { + const params = new TextureMapping.Params({ + useConstantLod: true, + constantLodProps: { + repetitions: 2.5, + offset: { x: 10, y: 20 }, + minDistClamp: 100, + maxDistClamp: 5000, + }, + }); + + expect(params.constantLodParams.repetitions).toBe(2.5); + expect(params.constantLodParams.offset).toEqual({ x: 10, y: 20 }); + expect(params.constantLodParams.minDistClamp).toBe(100); + expect(params.constantLodParams.maxDistClamp).toBe(5000); + }); + }); +}); diff --git a/core/frontend/src/common/gltf/GltfSchema.ts b/core/frontend/src/common/gltf/GltfSchema.ts index 74dcb7ad2c94..88a4b560d16c 100644 --- a/core/frontend/src/common/gltf/GltfSchema.ts +++ b/core/frontend/src/common/gltf/GltfSchema.ts @@ -398,6 +398,15 @@ export interface GltfTextureInfo extends GltfProperty { * Default: 0. */ texCoord?: number; + extensions?: GltfExtensions & { + // eslint-disable-next-line @typescript-eslint/naming-convention + EXT_textureInfo_constant_lod?: { + repetitions?: number, + offset?: [number, number], + minClampDistance?: number, + maxClampDistance?: number + } + }; } /** Describes a texture and its sampler. diff --git a/core/frontend/src/common/internal/render/MeshPrimitives.ts b/core/frontend/src/common/internal/render/MeshPrimitives.ts index aa48212b12f8..5c2eac06ef92 100644 --- a/core/frontend/src/common/internal/render/MeshPrimitives.ts +++ b/core/frontend/src/common/internal/render/MeshPrimitives.ts @@ -89,7 +89,9 @@ export function createMeshArgs(mesh: Mesh): MeshArgs | undefined { return undefined; const texture = mesh.displayParams.textureMapping?.texture; - const textureMapping = texture && mesh.uvParams.length > 0 ? { texture, uvParams: mesh.uvParams } : undefined; + const useConstantLod = mesh.displayParams.textureMapping?.params?.useConstantLod; + const constantLodParams = mesh.displayParams.textureMapping?.params?.constantLodParams; + const textureMapping = texture && mesh.uvParams.length > 0 ? { texture, uvParams: mesh.uvParams, useConstantLod, constantLodParams } : undefined; const colors = new ColorIndex(); mesh.colorMap.toColorIndex(colors, mesh.colors); diff --git a/core/frontend/src/render/MeshArgs.ts b/core/frontend/src/render/MeshArgs.ts index 86481733f119..7bf7368146c0 100644 --- a/core/frontend/src/render/MeshArgs.ts +++ b/core/frontend/src/render/MeshArgs.ts @@ -6,7 +6,7 @@ * @module Rendering */ -import { ColorIndex, FeatureIndex, FillFlags, OctEncodedNormal, QPoint3dList, RenderMaterial, RenderTexture } from "@itwin/core-common"; +import { ColorIndex, FeatureIndex, FillFlags, OctEncodedNormal, QPoint3dList, RenderMaterial, RenderTexture, TextureMapping } from "@itwin/core-common"; import { MeshArgsEdges } from "../common/internal/render/MeshPrimitives"; import { AuxChannel, Point2d, Point3d, Range3d } from "@itwin/core-geometry"; @@ -61,6 +61,14 @@ export interface MeshArgs { texture: RenderTexture; /** The per-vertex texture coordinates, indexed by [[vertIndices]]. */ uvParams: Point2d[]; + /** True if want to use constant LOD texture mapping for the surface texture. + * Default: false. + */ + useConstantLod?: boolean; + /** Parameters for constant LOD mapping mode. + * See [[TextureMapping.ConstantLodParamProps]] for defaults. + */ + constantLodParams?: TextureMapping.ConstantLodParamProps; }; } diff --git a/core/frontend/src/test/tile/GltfReader.test.ts b/core/frontend/src/test/tile/GltfReader.test.ts index 09287a04479b..28a995800f03 100644 --- a/core/frontend/src/test/tile/GltfReader.test.ts +++ b/core/frontend/src/test/tile/GltfReader.test.ts @@ -7,11 +7,11 @@ import { Range3d } from "@itwin/core-geometry"; import { EmptyLocalization, GltfV2ChunkTypes, GltfVersions, RenderTexture, TileFormat } from "@itwin/core-common"; import { IModelConnection } from "../../IModelConnection"; import { IModelApp } from "../../IModelApp"; -import { GltfDataType, GltfDocument, GltfId, GltfMesh, GltfMeshMode, GltfMeshPrimitive, GltfNode, GltfSampler, GltfWrapMode } from "../../common/gltf/GltfSchema"; +import { Gltf2Material, GltfDataType, GltfDocument, GltfId, GltfMesh, GltfMeshMode, GltfMeshPrimitive, GltfNode, GltfSampler, GltfWrapMode } from "../../common/gltf/GltfSchema"; import { getMeshPrimitives, GltfDataBuffer, GltfGraphicsReader, GltfReader, GltfReaderArgs, GltfReaderProps, GltfReaderResult } from "../../tile/GltfReader"; import { createBlankConnection } from "../createBlankConnection"; import { BatchedTileIdMap } from "../../tile/internal"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -1506,7 +1506,6 @@ const meshFeaturesExt: GltfDocument = JSON.parse(` }, }, }, "fallback"); - }); it("if a given primitive appears more than once in the same group", () => { @@ -1644,6 +1643,294 @@ const meshFeaturesExt: GltfDocument = JSON.parse(` }); }); + describe("EXT_textureInfo_constant_lod", () => { + const constantLodDoc: GltfDocument = JSON.parse(` +{ + "asset": { "version": "2.0" }, + "extensionsUsed": ["EXT_textureInfo_constant_lod"], + "extensionsRequired": ["EXT_textureInfo_constant_lod"], + "scene": 0, + "scenes": [{ "nodes": [0] }], + "nodes": [{ "mesh": 0 }], + "meshes": [{ + "primitives": [{ + "attributes": { "POSITION": 0 }, + "indices": 1, + "material": 0 + }] + }], + "materials": [{ + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0, + "extensions": { + "EXT_textureInfo_constant_lod": { + "repetitions": 2.5, + "offset": [10.0, 20.0], + "minClampDistance": 100.0, + "maxClampDistance": 5000.0 + } + } + } + } + }], + "textures": [{ "source": 0 }], + "images": [{ "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" }], + "accessors": [ + { "bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3", "min": [0, 0, 0], "max": [1, 1, 0] }, + { "bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR" } + ], + "bufferViews": [ + { "buffer": 0, "byteOffset": 0, "byteLength": 36 }, + { "buffer": 0, "byteOffset": 36, "byteLength": 6 } + ], + "buffers": [{ "byteLength": 42 }] +} +`); + + it("reads constant LOD properties", async () => { + // Add simple triangle vertex data + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(constantLodDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + expect(doc.materials).toBeDefined(); + + const material = doc.materials![0] as Gltf2Material; + expect(material.pbrMetallicRoughness?.baseColorTexture).toBeDefined(); + + const ext = material.pbrMetallicRoughness?.baseColorTexture?.extensions?.EXT_textureInfo_constant_lod; + expect(ext).toBeDefined(); + expect(ext?.repetitions).toBe(2.5); + expect(ext?.offset).toEqual([10.0, 20.0]); + expect(ext?.minClampDistance).toBe(100.0); + expect(ext?.maxClampDistance).toBe(5000.0); + }); + + it("reads extension even when properties are omitted", async () => { + const emptyExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + (emptyExtDoc.materials![0] as Gltf2Material).pbrMetallicRoughness!.baseColorTexture!.extensions!.EXT_textureInfo_constant_lod = {}; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(emptyExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const material = doc.materials![0] as Gltf2Material; + const ext = material.pbrMetallicRoughness?.baseColorTexture?.extensions?.EXT_textureInfo_constant_lod; + + expect(ext).toBeDefined(); + + // All properties should be undefined (defaults are applied by TextureMapping.Params constructor) + expect(ext?.repetitions).toBeUndefined(); + expect(ext?.offset).toBeUndefined(); + expect(ext?.minClampDistance).toBeUndefined(); + expect(ext?.maxClampDistance).toBeUndefined(); + }); + + it("reads extension from emissiveTexture when baseColorTexture is not present", async () => { + const emissiveExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + const material = emissiveExtDoc.materials![0] as Gltf2Material; + delete material.pbrMetallicRoughness!.baseColorTexture; + + // Add emissiveTexture with extension + material.emissiveTexture = { + index: 0, + extensions: { + EXT_textureInfo_constant_lod: { + repetitions: 3.0, + offset: [5.0, 15.0], + minClampDistance: 50.0, + maxClampDistance: 2500.0, + }, + }, + }; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(emissiveExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const mat = doc.materials![0] as Gltf2Material; + expect(mat.pbrMetallicRoughness?.baseColorTexture).toBeUndefined(); + + const ext = mat.emissiveTexture?.extensions?.EXT_textureInfo_constant_lod; + expect(ext).toBeDefined(); + expect(ext?.repetitions).toBe(3.0); + expect(ext?.offset).toEqual([5.0, 15.0]); + expect(ext?.minClampDistance).toBe(50.0); + expect(ext?.maxClampDistance).toBe(2500.0); + }); + + it("does not use emissiveTexture extension when baseColorTexture exists without extension", async () => { + const mixedExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + const material = mixedExtDoc.materials![0] as Gltf2Material; + + // Remove extension from baseColorTexture but keep baseColorTexture itself + delete material.pbrMetallicRoughness!.baseColorTexture!.extensions; + + // Add emissiveTexture with extension - this should NOT be used since baseColorTexture exists + material.emissiveTexture = { + index: 0, + extensions: { + EXT_textureInfo_constant_lod: {}, + }, + }; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(mixedExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const mat = doc.materials![0] as Gltf2Material; + + // baseColorTexture exists but has no extension + expect(mat.pbrMetallicRoughness?.baseColorTexture).toBeDefined(); + expect(mat.pbrMetallicRoughness?.baseColorTexture?.extensions?.EXT_textureInfo_constant_lod).toBeUndefined(); + + // emissiveTexture has extension but should not be used + expect(mat.emissiveTexture?.extensions?.EXT_textureInfo_constant_lod).toBeDefined(); + + const findTextureMappingSpy = vi.spyOn(reader as any, "findTextureMapping"); + (reader as any).createDisplayParams(mat, false); + + // constantLodParamProps (4th argument) should be undefined since we use baseColorTexture which has no extension + expect(findTextureMappingSpy).toHaveBeenCalled(); + expect(findTextureMappingSpy).toHaveBeenCalledWith(expect.any(String), false, undefined, undefined, false); + + vi.restoreAllMocks(); + }); + + it("enables constant LOD for normalTexture when both baseColorTexture and normalTexture have extension", async () => { + const normalExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + const material = normalExtDoc.materials![0] as Gltf2Material; + + material.normalTexture = { + index: 0, + extensions: { + EXT_textureInfo_constant_lod: { + repetitions: 4.0, + }, + }, + }; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(normalExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const mat = doc.materials![0] as Gltf2Material; + + const findTextureMappingSpy = vi.spyOn(reader as any, "findTextureMapping"); + (reader as any).createDisplayParams(mat, false); + + // normalMapUseConstantLod (5th argument) should be true when both textures have extension + expect(findTextureMappingSpy).toHaveBeenCalled(); + expect(findTextureMappingSpy).toHaveBeenCalledWith(expect.any(String), false, expect.any(String), expect.anything(), true); + + vi.restoreAllMocks(); + }); + + it("does not enable constant LOD for normalTexture when only normalTexture has extension", async () => { + const normalOnlyExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + const material = normalOnlyExtDoc.materials![0] as Gltf2Material; + + // Remove extension from baseColorTexture + delete material.pbrMetallicRoughness!.baseColorTexture!.extensions; + + // Add normalTexture with extension - should NOT enable constant LOD since base texture doesn't have it + material.normalTexture = { + index: 0, + extensions: { + EXT_textureInfo_constant_lod: { + repetitions: 4.0, + }, + }, + }; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(normalOnlyExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const mat = doc.materials![0] as Gltf2Material; + + const findTextureMappingSpy = vi.spyOn(reader as any, "findTextureMapping"); + (reader as any).createDisplayParams(mat, false); + + // normalMapUseConstantLod (5th argument) should be false + expect(findTextureMappingSpy).toHaveBeenCalled(); + expect(findTextureMappingSpy).toHaveBeenCalledWith(expect.any(String), false, expect.any(String), undefined, false); + + vi.restoreAllMocks(); + }); + + it("does not enable constant LOD when extension is not present", async () => { + const noExtDoc: GltfDocument = JSON.parse(JSON.stringify(constantLodDoc)); + const material = noExtDoc.materials![0] as Gltf2Material; + + // Remove extension from baseColorTexture and document-level declarations + delete material.pbrMetallicRoughness!.baseColorTexture!.extensions; + delete noExtDoc.extensionsUsed; + delete noExtDoc.extensionsRequired; + + const binaryData = new Uint8Array(42); + const floatView = new Float32Array(binaryData.buffer, 0, 9); + floatView.set([0, 0, 0, 1, 0, 0, 0, 1, 0]); + const indexView = new Uint16Array(binaryData.buffer, 36, 3); + indexView.set([0, 1, 2]); + + const reader = createReader(makeGlb(noExtDoc, binaryData))!; + expect(reader).toBeDefined(); + + const doc = (reader as any)._glTF as GltfDocument; + const mat = doc.materials![0] as Gltf2Material; + + expect(mat.pbrMetallicRoughness?.baseColorTexture?.extensions?.EXT_textureInfo_constant_lod).toBeUndefined(); + expect(mat.emissiveTexture?.extensions?.EXT_textureInfo_constant_lod).toBeUndefined(); + + const findTextureMappingSpy = vi.spyOn(reader as any, "findTextureMapping"); + (reader as any).createDisplayParams(mat, false); + + // constantLodParamProps (4th argument to findTextureMapping) should be undefined when extension is not present + expect(findTextureMappingSpy).toHaveBeenCalled(); + expect(findTextureMappingSpy).toHaveBeenCalledWith(expect.any(String), false, undefined, undefined, false); + + vi.restoreAllMocks(); + }); + }); + describe("Reality Tile Loader", () => { //These tests emulate way a reader is created by the reality tile loader diff --git a/core/frontend/src/tile/GltfReader.ts b/core/frontend/src/tile/GltfReader.ts index 29f246b708e1..c14d5cc685cc 100644 --- a/core/frontend/src/tile/GltfReader.ts +++ b/core/frontend/src/tile/GltfReader.ts @@ -13,9 +13,9 @@ import { Angle, IndexedPolyface, Matrix3d, Point2d, Point3d, Point4d, Range2d, Range3d, Transform, Vector3d, } from "@itwin/core-geometry"; import { - AxisAlignedBox3d, BatchType, ColorDef, EdgeAppearanceOverrides, ElementAlignedBox3d, Feature, FeatureIndex, FeatureIndexType, FeatureTable, FillFlags, GlbHeader, ImageSource, LinePixels, MeshEdge, - MeshEdges, MeshPolyline, MeshPolylineList, OctEncodedNormal, OctEncodedNormalPair, PackedFeatureTable, QParams2d, QParams3d, QPoint2dList, - QPoint3dList, Quantization, RenderMaterial, RenderMode, RenderTexture, TextureMapping, TextureTransparency, TileFormat, TileReadStatus, ViewFlagOverrides, + AxisAlignedBox3d, BatchType, ColorDef, EdgeAppearanceOverrides, ElementAlignedBox3d, Feature, FeatureIndex, FeatureIndexType, FeatureTable, FillFlags, GlbHeader, ImageSource, LinePixels, + MeshEdge, MeshEdges, MeshPolyline, MeshPolylineList, OctEncodedNormal, OctEncodedNormalPair, PackedFeatureTable, QParams2d, QParams3d, + QPoint2dList, QPoint3dList, Quantization, RenderMaterial, RenderMode, RenderTexture, TextureMapping, TextureTransparency, TileFormat, TileReadStatus, ViewFlagOverrides } from "@itwin/core-common"; import { IModelConnection } from "../IModelConnection"; import { IModelApp } from "../IModelApp"; @@ -45,6 +45,7 @@ import { GraphicTemplate } from "../render/GraphicTemplate"; import { LayerTileData } from "../internal/render/webgl/MapLayerParams"; import { compactEdgeIterator } from "../common/imdl/CompactEdges"; import { MeshPolylineGroup } from "@itwin/core-common/lib/cjs/internal/RenderMesh"; +import { MaterialTextureMappingProps } from "../common/render/MaterialParams"; /** @internal */ export type GltfDataBuffer = Uint8Array | Uint16Array | Uint32Array | Float32Array | Int8Array; @@ -1175,19 +1176,54 @@ export abstract class GltfReader { } protected createDisplayParams(material: GltfMaterial, hasBakedLighting: boolean, isPointPrimitive = false): DisplayParams | undefined { + let constantLodParamProps: TextureMapping.ConstantLodParamProps | undefined; + let normalMapUseConstantLod = false; + if (!isGltf1Material(material)) { + // NOTE: EXT_textureInfo_constant_lod is not supported for occlusionTexture and metallicRoughnessTexture + + // Use the same texture fallback logic as extractTextureId + const textureInfo = material.pbrMetallicRoughness?.baseColorTexture ?? material.emissiveTexture; + const extConstantLod = textureInfo?.extensions?.EXT_textureInfo_constant_lod; + const offset = extConstantLod?.offset; + extConstantLod ? constantLodParamProps = { + repetitions: extConstantLod?.repetitions, + offset: offset ? { x: offset[0], y: offset[1] } : undefined, + minDistClamp: extConstantLod?.minClampDistance, + maxDistClamp: extConstantLod?.maxClampDistance, + } : undefined; + // Normal map only uses constant LOD if both the base texture and normal texture have the extension + normalMapUseConstantLod = extConstantLod !== undefined && material.normalTexture?.extensions?.EXT_textureInfo_constant_lod !== undefined; + } + const isTransparent = this.isMaterialTransparent(material); const textureId = this.extractTextureId(material); const normalMapId = this.extractNormalMapId(material); - let textureMapping = (undefined !== textureId || undefined !== normalMapId) ? this.findTextureMapping(textureId, isTransparent, normalMapId) : undefined; + let textureMapping = (undefined !== textureId || undefined !== normalMapId) ? this.findTextureMapping(textureId, isTransparent, normalMapId, constantLodParamProps, normalMapUseConstantLod) : undefined; const color = colorFromMaterial(material, isTransparent); let renderMaterial: RenderMaterial | undefined; - if (undefined !== textureMapping && undefined !== textureMapping.normalMapParams) { - const args: CreateRenderMaterialArgs = { diffuse: { color }, specular: { color: ColorDef.white }, textureMapping }; + + if (undefined !== textureMapping) { + // Convert result of findTextureMapping (TextureMapping object) to MaterialTextureMappingProps interface + const textureMappingProps: MaterialTextureMappingProps = { + texture: textureMapping.texture, + normalMapParams: textureMapping.normalMapParams, + mode: textureMapping.params.mode, + transform: textureMapping.params.textureMatrix, + weight: textureMapping.params.weight, + worldMapping: textureMapping.params.worldMapping, + useConstantLod: textureMapping.params.useConstantLod, + constantLodProps: textureMapping.params.useConstantLod ? { + repetitions: textureMapping.params.constantLodParams.repetitions, + offset: textureMapping.params.constantLodParams.offset, + minDistClamp: textureMapping.params.constantLodParams.minDistClamp, + maxDistClamp: textureMapping.params.constantLodParams.maxDistClamp, + } : undefined, + }; + const args: CreateRenderMaterialArgs = { diffuse: { color }, specular: { color: ColorDef.white }, textureMapping: textureMappingProps }; renderMaterial = IModelApp.renderSystem.createRenderMaterial(args); // DisplayParams doesn't want a separate texture mapping if the material already has one. textureMapping = undefined; - } let width = 1; @@ -2305,7 +2341,7 @@ export abstract class GltfReader { return renderTexture ?? false; } - protected findTextureMapping(id: string | undefined, isTransparent: boolean, normalMapId: string | undefined): TextureMapping | undefined { + protected findTextureMapping(id: string | undefined, isTransparent: boolean, normalMapId: string | undefined, constantLodParamProps: TextureMapping.ConstantLodParamProps | undefined, normalMapUseConstantLod = false): TextureMapping | undefined { if (undefined === id && undefined === normalMapId) return undefined; @@ -2330,17 +2366,19 @@ export abstract class GltfReader { nMap = { normalMap, greenUp, + useConstantLod: normalMapUseConstantLod, }; } else { texture = normalMap; - nMap = { greenUp }; + nMap = { greenUp, useConstantLod: normalMapUseConstantLod }; } } if (!texture) return undefined; - const textureMapping = new TextureMapping(texture, new TextureMapping.Params()); + const useConstantLod = constantLodParamProps !== undefined; + const textureMapping = new TextureMapping(texture, new TextureMapping.Params({ useConstantLod, constantLodProps: constantLodParamProps })); textureMapping.normalMapParams = nMap; return textureMapping; } diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index c951a2786b7c..c52e0314e9e2 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -116,3 +116,13 @@ appendTextAnnotationGeometry({ ``` The render priority values are added to [SubCategoryAppearance.priority]($common) to determine the final display priority. This allows text annotations to render correctly relative to other 2D graphics. Note that render priorities have no effect in 3D views. + +## Display + +### EXT_textureInfo_constant_lod + +Support was added for the proposed [EXT_textureInfo_constant_lod](https://github.com/CesiumGS/glTF/pull/92) glTF extension which supports constant level-of-detail texture mapping mode for glTF models. The mode is already supported for iModels, see the [documentation](https://www.itwinjs.org/changehistory/4.0.0/#constant-lod-mapping-mode) from when it was introduced for more information. + +iTwin.js supports `EXT_textureInfo_constant_lod` on the `baseColorTexture` property in glTF model materials, with fallback to `emissiveTexture` if `baseColorTexture` is not present. When the extension is present on `normalTexture`, it is only applied when `baseColorTexture` (or `emissiveTexture`) also has the extension, and the constant LOD properties from the base texture are used for both to keep texture mapping in sync. + +The extension is not supported for `occlusionTexture` and `metallicRoughnessTexture`.