From d4062a6511d1788acc57dc78a57b1ee468d8566e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Ter=C3=A1n=20R=C3=ADos?= Date: Thu, 4 May 2023 20:01:02 +0300 Subject: [PATCH] Get and Set texture transform (#4209) * Get and Set texture transform * Adds feature #3368 * Adds modelviewer.dev example * Texture Transform in Sampler * constructor fix * Sampler API working Missing automated test * Add test * Document and clean gltf file. * Revoke url from test * Remove test.only * Fix coding style. * Clean index and test file --- .../src/features/scene-graph/api.ts | 41 ++++++++- .../src/features/scene-graph/sampler.ts | 62 ++++++++++++-- .../features/scene-graph/texture-info-spec.ts | 29 +++++++ .../gltf-instance/gltf-defaulted.ts | 5 ++ packages/modelviewer.dev/data/examples.json | 6 +- .../examples/scenegraph/index.html | 83 ++++++++++++++++++- 6 files changed, 215 insertions(+), 11 deletions(-) diff --git a/packages/model-viewer/src/features/scene-graph/api.ts b/packages/model-viewer/src/features/scene-graph/api.ts index 4bbf410af0..ee44ee7306 100644 --- a/packages/model-viewer/src/features/scene-graph/api.ts +++ b/packages/model-viewer/src/features/scene-graph/api.ts @@ -16,7 +16,6 @@ import {AlphaMode, MagFilter, MinFilter, WrapMode} from '../../three-components/gltf-instance/gltf-2.0.js'; - /** * All constructs in a 3DOM scene graph have a corresponding string name. * This is similar in spirit to the concept of a "tag name" in HTML, and exists @@ -32,6 +31,12 @@ export declare interface ThreeDOMElementMap { 'texture-info': TextureInfo; } +/** A 2D Cartesian coordinate */ +export interface Vector2 { + x: number; + y: number; +} + /** * A Model is the root element of a 3DOM scene graph. It gives scripts access * to the sub-elements found without the graph. @@ -201,7 +206,7 @@ export declare interface PBRMetallicRoughness { */ export declare interface TextureInfo { /** - * The Texture being referenced by this TextureInfo + * The Texture being referenced by this TextureInfo. */ readonly texture: Texture|null; @@ -266,6 +271,21 @@ export declare interface Sampler { */ readonly wrapT: WrapMode; + /** + * The texture rotation in radians. + */ + readonly rotation: number|null; + + /** + * The texture scale. + */ + readonly scale: Vector2|null; + + /** + * The texture offset. + */ + readonly offset: Vector2|null; + /** * Configure the minFilter value of the Sampler. */ @@ -285,6 +305,23 @@ export declare interface Sampler { * Configure the T (V) wrap mode of the Sampler. */ setWrapT(mode: WrapMode): void; + + /** + * Sets the texture rotation, or resets it to zero if argument is null. + * Rotation is in radians, positive for counter-clockwise. + */ + setRotation(rotation: number|null): void; + + /** + * Sets the texture scale, or resets it to (1, 1) if argument is null. + * As the scale value increases, the repetition of the texture will increase. + */ + setScale(scale: Vector2|null): void; + + /** + * Sets the texture offset, or resets it to (0, 0) if argument is null. + */ + setOffset(offset: Vector2|null): void; } diff --git a/packages/model-viewer/src/features/scene-graph/sampler.ts b/packages/model-viewer/src/features/scene-graph/sampler.ts index c59cdccb6e..779ac70d6a 100644 --- a/packages/model-viewer/src/features/scene-graph/sampler.ts +++ b/packages/model-viewer/src/features/scene-graph/sampler.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import {Texture as ThreeTexture} from 'three'; +import {Texture as ThreeTexture, Vector2} from 'three'; import {Filter, MagFilter, MinFilter, Sampler as GLTFSampler, Wrap, WrapMode} from '../../three-components/gltf-instance/gltf-2.0.js'; import {Sampler as DefaultedSampler} from '../../three-components/gltf-instance/gltf-defaulted.js'; @@ -49,7 +49,7 @@ const isWrapMode = (() => { wrapModes.indexOf(value as WrapMode) > -1; })(); -const isValidSamplerValue =

( +const isValidSamplerValue =

( property: P, value: unknown): value is DefaultedSampler[P] => { switch (property) { case 'minFilter': @@ -59,11 +59,16 @@ const isValidSamplerValue =

( case 'wrapS': case 'wrapT': return isWrapMode(value); + case 'rotation': + case 'repeat': + case 'offset': + return true; default: throw new Error(`Cannot configure property "${property}" on Sampler`); } }; +const $threeTexture = Symbol('threeTexture'); const $threeTextures = Symbol('threeTextures'); const $setProperty = Symbol('setProperty'); const $sourceSampler = Symbol('sourceSampler'); @@ -72,6 +77,13 @@ const $sourceSampler = Symbol('sourceSampler'); * Sampler facade implementation for Three.js textures */ export class Sampler extends ThreeDOMElement implements SamplerInterface { + private get[$threeTexture]() { + console.assert( + this[$correlatedObjects] != null && this[$correlatedObjects]!.size > 0, + 'Sampler correlated object is undefined'); + return this[$correlatedObjects]?.values().next().value as ThreeTexture; + } + private get[$threeTextures]() { console.assert( this[$correlatedObjects] != null && this[$correlatedObjects]!.size > 0, @@ -132,6 +144,18 @@ export class Sampler extends ThreeDOMElement implements SamplerInterface { return this[$sourceSampler].wrapT; } + get rotation(): number { + return this[$threeTexture].rotation; + } + + get scale(): Vector2 { + return this[$threeTexture].repeat; + } + + get offset(): Vector2|null { + return this[$threeTexture].offset; + } + setMinFilter(filter: MinFilter) { this[$setProperty]('minFilter', filter); } @@ -148,15 +172,41 @@ export class Sampler extends ThreeDOMElement implements SamplerInterface { this[$setProperty]('wrapT', mode); } - private[$setProperty]

( - property: P, value: MinFilter|MagFilter|WrapMode) { + setRotation(rotation: number|null): void { + if(rotation == null) { + // Reset rotation. + rotation = 0; + } + this[$setProperty]('rotation', rotation); + } + + setScale(scale: Vector2|null): void { + if(scale == null) { + // Reset scale. + scale = new Vector2(1, 1); + } + this[$setProperty]('repeat', scale); + } + + setOffset(offset: Vector2|null): void { + if(offset == null) { + // Reset offset. + offset = new Vector2(0, 0); + } + this[$setProperty]('offset', offset); + } + + private[$setProperty]

( + property: P, value: MinFilter|MagFilter|WrapMode|number|Vector2) { const sampler = this[$sourceSampler]; if (sampler != null) { if (isValidSamplerValue(property, value)) { - sampler[property] = value; + if (property !== 'rotation' && property !== 'repeat' && property !== 'offset') { + sampler[property] = value; + } for (const texture of this[$threeTextures]) { - (texture[property] as MinFilter | MagFilter | WrapMode) = value; + (texture[property] as MinFilter | MagFilter | WrapMode | number | Vector2) = value; texture.needsUpdate = true; } } diff --git a/packages/model-viewer/src/test/features/scene-graph/texture-info-spec.ts b/packages/model-viewer/src/test/features/scene-graph/texture-info-spec.ts index 687ac97d63..ccebb37682 100644 --- a/packages/model-viewer/src/test/features/scene-graph/texture-info-spec.ts +++ b/packages/model-viewer/src/test/features/scene-graph/texture-info-spec.ts @@ -13,6 +13,7 @@ * limitations under the License. */ +import { Vector2 } from 'three'; import {TextureInfo} from '../../../features/scene-graph/texture-info.js'; import {ModelViewerElement} from '../../../model-viewer.js'; import {waitForEvent} from '../../../utilities.js'; @@ -21,6 +22,7 @@ import {assetPath} from '../../helpers.js'; const expect = chai.expect; const DUCK_GLB_PATH = assetPath('models/glTF-Sample-Models/2.0/Duck/glTF-Binary/Duck.glb'); +const TEXTURED_CUBE_GLB_PATH = assetPath('models/glTF-Sample-Models/2.0/BoxTextured/glTF-Binary/BoxTextured.glb'); suite('scene-graph/texture-info', () => { suite('texture-info', () => { @@ -65,5 +67,32 @@ suite('scene-graph/texture-info', () => { emptyTextureInfo.setTexture(null); expect(emptyTextureInfo.texture).to.be.null; }); + + test('exports and re-imports the model with transformed texture', async () => { + // Load textured glb. + element.src = TEXTURED_CUBE_GLB_PATH; + await waitForEvent(element, 'load'); + + // Transform the textures. + const sampler = element.model?.materials[0].pbrMetallicRoughness['baseColorTexture'].texture?.sampler!; + sampler.setRotation(0.1); + sampler.setOffset(new Vector2(0.2, 0.3)); + sampler.setScale(new Vector2(0.4, 0.5)); + + // Export model. + const exported = await element.exportScene({binary: true}); + const url = URL.createObjectURL(exported); + + // Re-load model. + element.src = url; + await waitForEvent(element, 'load'); + + URL.revokeObjectURL(url); + + const exported_sampler = element.model?.materials[0].pbrMetallicRoughness['baseColorTexture'].texture?.sampler!; + expect(exported_sampler.rotation).to.be.eq(0.1, 'rotation'); + expect(exported_sampler.offset).to.be.eql(new Vector2(0.2, 0.3), 'offset'); + expect(exported_sampler.scale).to.be.eql(new Vector2(0.4, 0.5), 'scale'); + }); }); }); diff --git a/packages/model-viewer/src/three-components/gltf-instance/gltf-defaulted.ts b/packages/model-viewer/src/three-components/gltf-instance/gltf-defaulted.ts index 4e274e6ce6..747dba6682 100644 --- a/packages/model-viewer/src/three-components/gltf-instance/gltf-defaulted.ts +++ b/packages/model-viewer/src/three-components/gltf-instance/gltf-defaulted.ts @@ -1,3 +1,5 @@ +import {Vector2} from 'three'; + import {Accessor, AlphaMode, AnimationSampler, Asset, Camera, ExtensionDictionary, Extras, MagFilter, Mesh, MinFilter, RGB, RGBA, Scene, WrapMode} from './gltf-2.0'; @@ -9,6 +11,9 @@ export interface Sampler { wrapT: WrapMode; extensions?: ExtensionDictionary; extras?: Extras; + rotation: number; + repeat: Vector2; + offset: Vector2; } export interface Texture { diff --git a/packages/modelviewer.dev/data/examples.json b/packages/modelviewer.dev/data/examples.json index 1d62d53eb1..9edc7c5163 100644 --- a/packages/modelviewer.dev/data/examples.json +++ b/packages/modelviewer.dev/data/examples.json @@ -248,9 +248,13 @@ "name": "Create Textures" }, { - "htmlId": "swapTextures", + "htmlId": "swapTexturesExample", "name": "Swap Textures" }, + { + "htmlId": "transformTexturesExample", + "name": "Transform Textures" + }, { "htmlId": "animatedTexturesExample", "name": "Animated Textures" diff --git a/packages/modelviewer.dev/examples/scenegraph/index.html b/packages/modelviewer.dev/examples/scenegraph/index.html index f53cf91570..6c9902d08a 100644 --- a/packages/modelviewer.dev/examples/scenegraph/index.html +++ b/packages/modelviewer.dev/examples/scenegraph/index.html @@ -379,7 +379,7 @@

Create textures

-
+
@@ -388,7 +388,7 @@

Swap textures

mode. iOS Quick Look reflects these texture changes so long as the USDZ is auto-generated.

- +