Skip to content

Commit a4639b5

Browse files
authored
Video texture (#3886)
* added createElementTexture and example * moved updates to setTexture * turn off video updates * added initial Lottie support * updated create textures api * todo * started dynamic import * back to bundled lottie for now * lazy load working * added docs * updated docs
1 parent ecd19aa commit a4639b5

File tree

17 files changed

+363
-36
lines changed

17 files changed

+363
-36
lines changed

packages/model-viewer/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/model-viewer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@
103103
"rollup-plugin-dts": "^4.2.2",
104104
"rollup-plugin-polyfill": "^3.0.0",
105105
"rollup-plugin-terser": "^7.0.2",
106-
"typescript": "4.8.4"
106+
"typescript": "4.8.4",
107+
"lottie-web": "5.9.6"
107108
},
108109
"publishConfig": {
109110
"access": "public"

packages/model-viewer/src/features/loading.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const DEFAULT_DRACO_DECODER_LOCATION =
3333
const DEFAULT_KTX2_TRANSCODER_LOCATION =
3434
'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
3535

36+
const DEFAULT_LOTTIE_LOADER_LOCATION =
37+
'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/LottieLoader.js';
38+
3639
const RevealStrategy: {[index: string]: RevealAttributeValue} = {
3740
AUTO: 'auto',
3841
MANUAL: 'manual'
@@ -74,13 +77,15 @@ export declare interface LoadingStaticInterface {
7477
dracoDecoderLocation: string;
7578
ktx2TranscoderLocation: string;
7679
meshoptDecoderLocation: string;
80+
lottieLoaderLocation: string;
7781
mapURLs(callback: (url: string) => string): void;
7882
}
7983

8084
export interface ModelViewerGlobalConfig {
8185
dracoDecoderLocation?: string;
8286
ktx2TranscoderLocation?: string;
8387
meshoptDecoderLocation?: string;
88+
lottieLoaderLocation?: string;
8489
powerPreference?: string;
8590
}
8691

@@ -161,6 +166,14 @@ export const LoadingMixin = <T extends Constructor<ModelViewerElementBase>>(
161166
return CachingGLTFLoader.getMeshoptDecoderLocation();
162167
}
163168

169+
static set lottieLoaderLocation(value: string) {
170+
Renderer.singleton.textureUtils!.lottieLoaderUrl = value;
171+
}
172+
173+
static get lottieLoaderLocation() {
174+
return Renderer.singleton.textureUtils!.lottieLoaderUrl
175+
}
176+
164177
/**
165178
* If provided, the callback will be passed each resource URL before a
166179
* request is sent. The callback may return the original URL, or a new URL
@@ -313,6 +326,10 @@ export const LoadingMixin = <T extends Constructor<ModelViewerElementBase>>(
313326
CachingGLTFLoader.setMeshoptDecoderLocation(
314327
ModelViewerElement.meshoptDecoderLocation);
315328
}
329+
330+
const lottieLoaderLocation = ModelViewerElement.lottieLoaderLocation ||
331+
DEFAULT_LOTTIE_LOADER_LOCATION;
332+
Renderer.singleton.textureUtils!.lottieLoaderUrl = lottieLoaderLocation;
316333
}
317334

318335
connectedCallback() {

packages/model-viewer/src/features/scene-graph.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import {property} from 'lit/decorators.js';
17-
import {RepeatWrapping, sRGBEncoding, Texture, TextureLoader} from 'three';
17+
import {CanvasTexture, RepeatWrapping, sRGBEncoding, Texture, VideoTexture} from 'three';
1818
import {GLTFExporter, GLTFExporterOptions} from 'three/examples/jsm/exporters/GLTFExporter.js';
1919

2020
import ModelViewerElementBase, {$needsRender, $onModelLoad, $progressTracker, $renderer, $scene} from '../model-viewer-base.js';
@@ -34,7 +34,7 @@ export const $currentGLTF = Symbol('currentGLTF');
3434
export const $originalGltfJson = Symbol('originalGltfJson');
3535
export const $model = Symbol('model');
3636
const $getOnUpdateMethod = Symbol('getOnUpdateMethod');
37-
const $textureLoader = Symbol('textureLoader');
37+
const $buildTexture = Symbol('buildTexture');
3838

3939
interface SceneExportOptions {
4040
binary?: boolean, trs?: boolean, onlyVisible?: boolean,
@@ -51,6 +51,10 @@ export interface SceneGraphInterface {
5151
readonly originalGltfJson: GLTF|null;
5252
exportScene(options?: SceneExportOptions): Promise<Blob>;
5353
createTexture(uri: string, type?: string): Promise<ModelViewerTexture|null>;
54+
createLottieTexture(uri: string, quality?: number):
55+
Promise<ModelViewerTexture|null>;
56+
createVideoTexture(uri: string): ModelViewerTexture;
57+
createCanvasTexture(): ModelViewerTexture;
5458
/**
5559
* Intersects a ray with the scene and returns a list of materials who's
5660
* objects were intersected.
@@ -70,7 +74,6 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
7074
class SceneGraphModelViewerElement extends ModelViewerElement {
7175
protected[$model]: Model|undefined = undefined;
7276
protected[$currentGLTF]: ModelViewerGLTFInstance|null = null;
73-
private[$textureLoader] = new TextureLoader();
7477
private[$originalGltfJson]: GLTF|null = null;
7578

7679
@property({type: String, attribute: 'variant-name'})
@@ -117,22 +120,48 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
117120
};
118121
}
119122

120-
async createTexture(uri: string, type: string = 'image/png'):
121-
Promise<ModelViewerTexture|null> {
122-
const currentGLTF = this[$currentGLTF];
123-
const texture: Texture = await new Promise<Texture>(
124-
(resolve) => this[$textureLoader].load(uri, resolve));
125-
if (!currentGLTF || !texture) {
126-
return null;
127-
}
128-
// Applies default settings.
123+
private[$buildTexture](texture: Texture): ModelViewerTexture {
124+
// Applies glTF default settings.
129125
texture.encoding = sRGBEncoding;
130126
texture.wrapS = RepeatWrapping;
131127
texture.wrapT = RepeatWrapping;
132-
texture.flipY = false;
128+
return new ModelViewerTexture(this[$getOnUpdateMethod](), texture);
129+
}
130+
131+
async createTexture(uri: string, type: string = 'image/png'):
132+
Promise<ModelViewerTexture> {
133+
const {textureUtils} = this[$renderer];
134+
const texture = await textureUtils!.loadImage(uri);
135+
133136
texture.userData.mimeType = type;
134137

135-
return new ModelViewerTexture(this[$getOnUpdateMethod](), texture);
138+
return this[$buildTexture](texture);
139+
}
140+
141+
async createLottieTexture(uri: string, quality = 1):
142+
Promise<ModelViewerTexture> {
143+
const {textureUtils} = this[$renderer];
144+
const texture = await textureUtils!.loadLottie(uri, quality);
145+
146+
return this[$buildTexture](texture);
147+
}
148+
149+
createVideoTexture(uri: string): ModelViewerTexture {
150+
const video = document.createElement('video');
151+
video.src = uri;
152+
video.muted = true;
153+
video.play();
154+
video.loop = true;
155+
const texture = new VideoTexture(video);
156+
157+
return this[$buildTexture](texture);
158+
}
159+
160+
createCanvasTexture(): ModelViewerTexture {
161+
const canvas = document.createElement('canvas');
162+
const texture = new CanvasTexture(canvas);
163+
164+
return this[$buildTexture](texture);
136165
}
137166

138167
async updated(changedProperties: Map<string, any>) {

packages/model-viewer/src/features/scene-graph/api.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* See the License for the specific language governing permissions and
1313
* limitations under the License.
1414
*/
15+
import {AnimationItem} from 'lottie-web';
16+
1517
import {AlphaMode, MagFilter, MinFilter, WrapMode} from '../../three-components/gltf-instance/gltf-2.0.js';
1618

1719

@@ -314,14 +316,30 @@ export declare interface Image {
314316
/**
315317
* The bufferView of the image, if it is embedded.
316318
*/
317-
readonly bufferView?: number
319+
readonly bufferView?: number;
320+
321+
/**
322+
* The backing HTML element, if this is a video or canvas texture.
323+
*/
324+
readonly element?: HTMLVideoElement|HTMLCanvasElement;
325+
326+
/**
327+
* The Lottie animation object, if this is a Lottie texture.
328+
*/
329+
readonly animation?: AnimationItem;
318330

319331
/**
320332
* A method to create an object URL of this image at the desired
321333
* resolution. Especially useful for KTX2 textures which are GPU compressed,
322334
* and so are unreadable on the CPU without a method like this.
323335
*/
324336
createThumbnail(width: number, height: number): Promise<string>;
337+
338+
/**
339+
* Only applies to canvas textures. Call when the content of the canvas has
340+
* been updated and should be reflected in the model.
341+
*/
342+
update(): void;
325343
}
326344

327345
/**

packages/model-viewer/src/features/scene-graph/image.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
* limitations under the License.
1414
*/
1515

16+
import {AnimationItem} from 'lottie-web';
1617
import {Mesh, MeshBasicMaterial, OrthographicCamera, PlaneGeometry, Scene, Texture as ThreeTexture, WebGLRenderTarget} from 'three';
1718

1819
import {blobCanvas} from '../../model-viewer-base.js';
1920
import {Image as GLTFImage} from '../../three-components/gltf-instance/gltf-2.0.js';
2021
import {Renderer} from '../../three-components/Renderer.js';
2122

2223
import {Image as ImageInterface} from './api.js';
23-
import {$correlatedObjects, $sourceObject, ThreeDOMElement} from './three-dom-element.js';
24+
import {$correlatedObjects, $onUpdate, $sourceObject, ThreeDOMElement} from './three-dom-element.js';
2425

2526

2627
const quadMaterial = new MeshBasicMaterial();
@@ -67,6 +68,22 @@ export class Image extends ThreeDOMElement implements ImageInterface {
6768
return (this[$sourceObject] as GLTFImage).bufferView;
6869
}
6970

71+
get element(): HTMLVideoElement|HTMLCanvasElement|undefined {
72+
const texture = this[$threeTexture] as any;
73+
if (texture && (texture.isCanvasTexture || texture.isVideoTexture)) {
74+
return texture.image;
75+
}
76+
return;
77+
}
78+
79+
get animation(): AnimationItem|undefined {
80+
const texture = this[$threeTexture] as any;
81+
if (texture && texture.isCanvasTexture && texture.animation) {
82+
return texture.animation;
83+
}
84+
return;
85+
}
86+
7087
get type(): 'embedded'|'external' {
7188
return this.uri != null ? 'external' : 'embedded';
7289
}
@@ -75,6 +92,15 @@ export class Image extends ThreeDOMElement implements ImageInterface {
7592
(this[$sourceObject] as GLTFImage).name = name;
7693
}
7794

95+
update() {
96+
const texture = this[$threeTexture] as any;
97+
// Applies to non-Lottie canvas textures only
98+
if (texture && texture.isCanvasTexture && !texture.animation) {
99+
this[$threeTexture].needsUpdate = true;
100+
this[$onUpdate]();
101+
}
102+
}
103+
78104
async createThumbnail(width: number, height: number): Promise<string> {
79105
const scene = new Scene();
80106
quadMaterial.map = this[$threeTexture];

packages/model-viewer/src/features/scene-graph/texture-info.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16-
import {LinearEncoding, MeshStandardMaterial, sRGBEncoding, Texture as ThreeTexture, TextureEncoding, Vector2} from 'three';
16+
import {LinearEncoding, MeshStandardMaterial, sRGBEncoding, Texture as ThreeTexture, TextureEncoding, Vector2, VideoTexture} from 'three';
1717

1818
import {GLTF, TextureInfo as GLTFTextureInfo} from '../../three-components/gltf-instance/gltf-2.0.js';
1919

@@ -25,6 +25,8 @@ const $texture = Symbol('texture');
2525
const $transform = Symbol('transform');
2626
export const $materials = Symbol('materials');
2727
export const $usage = Symbol('usage');
28+
const $onUpdate = Symbol('onUpdate');
29+
const $activeVideo = Symbol('activeVideo');
2830

2931
// Defines what a texture will be used for.
3032
export enum TextureUsage {
@@ -58,7 +60,8 @@ export class TextureInfo implements TextureInfoInterface {
5860
// Texture usage defines the how the texture is used (ie Normal, Emissive...
5961
// etc)
6062
[$usage]: TextureUsage;
61-
onUpdate: () => void;
63+
[$onUpdate]: () => void;
64+
[$activeVideo] = false;
6265

6366
constructor(
6467
onUpdate: () => void, usage: TextureUsage,
@@ -83,7 +86,7 @@ export class TextureInfo implements TextureInfoInterface {
8386
new Texture(onUpdate, threeTexture, gltfTexture, sampler, image);
8487
}
8588

86-
this.onUpdate = onUpdate;
89+
this[$onUpdate] = onUpdate;
8790
this[$materials] = material;
8891
this[$usage] = usage;
8992
}
@@ -95,9 +98,36 @@ export class TextureInfo implements TextureInfoInterface {
9598
setTexture(texture: Texture|null): void {
9699
const threeTexture: ThreeTexture|null =
97100
texture != null ? texture.source[$threeTexture] : null;
98-
let encoding: TextureEncoding = sRGBEncoding;
101+
102+
const oldTexture = this[$texture] as unknown as VideoTexture;
103+
if (oldTexture != null && oldTexture.isVideoTexture) {
104+
const element = oldTexture.image;
105+
this[$activeVideo] = false;
106+
if (element.requestVideoFrameCallback == null) {
107+
element.removeEventListener('timeupdate', this[$onUpdate]);
108+
}
109+
}
110+
99111
this[$texture] = texture;
100112

113+
if (threeTexture != null && (threeTexture as VideoTexture).isVideoTexture) {
114+
const element = threeTexture.image;
115+
this[$activeVideo] = true;
116+
if (element.requestVideoFrameCallback != null) {
117+
const update = () => {
118+
if (!this[$activeVideo]) {
119+
return;
120+
}
121+
this[$onUpdate]();
122+
element.requestVideoFrameCallback(update);
123+
};
124+
element.requestVideoFrameCallback(update);
125+
} else {
126+
element.addEventListener('timeupdate', this[$onUpdate]);
127+
}
128+
}
129+
130+
let encoding: TextureEncoding = sRGBEncoding;
101131
if (this[$materials]) {
102132
for (const material of this[$materials]!) {
103133
switch (this[$usage]) {
@@ -133,6 +163,6 @@ export class TextureInfo implements TextureInfoInterface {
133163
threeTexture.repeat = this[$transform].scale;
134164
threeTexture.offset = this[$transform].offset;
135165
}
136-
this.onUpdate();
166+
this[$onUpdate]();
137167
}
138168
}

packages/model-viewer/src/model-viewer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import {StagingMixin} from './features/staging.js';
2424
import ModelViewerElementBase from './model-viewer-base.js';
2525
import {FocusVisiblePolyfillMixin} from './utilities/focus-visible.js';
2626

27-
// Uncomment these lines to export PMREM textures in Glitch:
28-
// export {default as TextureUtils} from './three-components/TextureUtils';
29-
// export * from 'three';
27+
// Export these to allow lazy-loaded LottieLoader.js to find what it needs.
28+
// Requires an import map - "three": "path/to/model-viewer.min.js".
29+
export {CanvasTexture, FileLoader, Loader, NearestFilter} from 'three';
3030

3131
export const ModelViewerElement = AnnotationMixin(SceneGraphMixin(StagingMixin(
3232
EnvironmentMixin(ControlsMixin(ARMixin(LoadingMixin(AnimationMixin(

0 commit comments

Comments
 (0)