Skip to content

Commit 990ea8b

Browse files
authored
feat: add variant
* feat: add variant * docs: add docs for variant
1 parent 6710aad commit 990ea8b

File tree

10 files changed

+203
-11
lines changed

10 files changed

+203
-11
lines changed

demo/docs/options/model/dracoPath.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 3
2+
sidebar_position: 4
33
---
44

55
import View3D from "@site/src/components/View3D";

demo/docs/options/model/ktxPath.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 4
2+
sidebar_position: 5
33
---
44

55
import View3D from "@site/src/components/View3D";

demo/docs/options/model/meshoptPath.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 5
2+
sidebar_position: 6
33
---
44

55
import View3D from "@site/src/components/View3D";

demo/docs/options/model/variant.mdx

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
sidebar_position: 3
3+
---
4+
5+
import View3D from "@site/src/components/View3D";
6+
import Variant from "@site/src/components/demo/Variant";
7+
import OptionDescriptor from "@site/src/components/OptionDescriptor";
8+
import License from "@site/src/components/License";
9+
10+
<OptionDescriptor type="string | number | null" defaultVal="null" />
11+
12+
Set material variant of the model, which only can be used when using [glTF materials variants extension](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_variants/README.md)
13+
Either can be index of the variant(number), or the name of the variant(string)
14+
15+
## Example
16+
<div className="columns">
17+
<div className="column is-6">
18+
<View3D
19+
src="/egjs-view3d/model/MaterialsVariantsShoe.glb"
20+
variant={null}
21+
showExampleCode />
22+
</div>
23+
<div className="column is-6">
24+
<View3D
25+
src="/egjs-view3d/model/MaterialsVariantsShoe.glb"
26+
variant={1}
27+
showExampleCode />
28+
</div>
29+
</div>
30+
<div className="columns">
31+
<div className="column is-6">
32+
<View3D
33+
src="/egjs-view3d/model/MaterialsVariantsShoe.glb"
34+
variant="street"
35+
showExampleCode />
36+
</div>
37+
<div className="column is-6"></div>
38+
</div>
39+
40+
## Changing Variant
41+
You can easily change variant by changing the option value.
42+
43+
<Variant />
44+
45+
```js
46+
// You can use index of the variant
47+
view3D.variant = 1;
48+
49+
// Or the name of the variant
50+
view3D.variant = "beach";
51+
```
52+
53+
<License items={[
54+
{
55+
name: "MaterialsVariantsShoe",
56+
link: "https://github.com/pushmatrix/glTF-Sample-Models/tree/master/2.0/MaterialsVariantsShoe",
57+
author: "Shopify",
58+
authorLink: "https://github.com/Shopify",
59+
license: "Creative Commons Attribution"
60+
}
61+
]} />

demo/src/components/demo/Variant.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { useRef, useState } from "react";
2+
import clsx from "clsx";
3+
import View3D, { TONE_MAPPING } from "../View3D";
4+
5+
const variants = [
6+
"midnight",
7+
"beach",
8+
"street"
9+
]
10+
11+
export default () => {
12+
const view3D = useRef<View3D>(null);
13+
14+
return <>
15+
<section>
16+
<div>
17+
<table>
18+
<tbody>
19+
<tr>
20+
<td className="mr-2">Variant:</td>
21+
<td>
22+
<div className={clsx({ select: true, "mr-1": true })}>
23+
<select defaultValue={0} onChange={e => {
24+
view3D.current!.view3D.variant = e.target.value;
25+
}}>
26+
{ variants.map((name, idx) => (
27+
<option key={idx} value={name}>{name}</option>
28+
)) }
29+
</select>
30+
</div>
31+
</td>
32+
</tr>
33+
</tbody>
34+
</table>
35+
</div>
36+
<View3D
37+
ref={view3D}
38+
src="/egjs-view3d/model/MaterialsVariantsShoe.glb" />
39+
</section>
40+
</>;
41+
};
7.47 MB
Binary file not shown.

packages/view3d/src/View3D.ts

+21
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface View3DOptions {
7171
// Model
7272
src: string | string[] | null;
7373
iosSrc: string | null;
74+
variant: number | string | null;
7475
dracoPath: string;
7576
ktxPath: string;
7677
meshoptPath: string | null;
@@ -167,6 +168,7 @@ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3
167168
// Options
168169
private _src: View3DOptions["src"];
169170
private _iosSrc: View3DOptions["iosSrc"];
171+
private _variant: View3DOptions["variant"];
170172
private _dracoPath: View3DOptions["dracoPath"];
171173
private _ktxPath: View3DOptions["ktxPath"];
172174
private _meshoptPath: View3DOptions["meshoptPath"];
@@ -313,6 +315,14 @@ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3
313315
* @default null
314316
*/
315317
public get iosSrc() { return this._iosSrc; }
318+
/**
319+
* Active material variant of the model.
320+
* Either can be index of the variant(number), or the name of the variant(string)
321+
* Changing this value will change the material of the model
322+
* @default null
323+
* @see https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_variants/README.md
324+
*/
325+
public get variant() { return this._variant; }
316326
/**
317327
* URL to {@link https://github.com/google/draco Draco} decoder location.
318328
* @type {string}
@@ -624,6 +634,15 @@ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3
624634
*/
625635
public get maxDeltaTime() { return this._maxDeltaTime; }
626636

637+
public set variant(val: View3DOptions["variant"]) {
638+
if (this._model) {
639+
this._model.selectVariant(val)
640+
.then(() => {
641+
this.renderer.renderSingleFrame();
642+
});
643+
}
644+
this._variant = val;
645+
}
627646
public set defaultAnimationIndex(val: View3DOptions["defaultAnimationIndex"]) { this._defaultAnimationIndex = val; }
628647
public set initialZoom(val: View3DOptions["initialZoom"]) { this._initialZoom = val; }
629648

@@ -704,6 +723,7 @@ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3
704723
public constructor(root: string | HTMLElement, {
705724
src = null,
706725
iosSrc = null,
726+
variant = null,
707727
dracoPath = DEFAULT.DRACO_DECODER_URL,
708728
ktxPath = DEFAULT.KTX_TRANSCODER_URL,
709729
meshoptPath = null,
@@ -758,6 +778,7 @@ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3
758778
// Bind options
759779
this._src = src;
760780
this._iosSrc = iosSrc;
781+
this._variant = variant;
761782
this._dracoPath = dracoPath;
762783
this._ktxPath = ktxPath;
763784
this._meshoptPath = meshoptPath;

packages/view3d/src/const/internal.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export enum GESTURE {
4444
PINCH = 16,
4545
}
4646

47+
export const VARIANT_EXTENSION = "KHR_materials_variants";
48+
4749
export const CUSTOM_TEXTURE_LOD_EXTENSION = "EXT_View3D_texture_LOD";
4850
export const TEXTURE_LOD_EXTRA = "view3d-lod";
4951
export const ANNOTATION_EXTRA = "view3d-annotation";

packages/view3d/src/core/Model.ts

+58-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
*/
55

66
import * as THREE from "three";
7+
import { GLTFParser } from "three/examples/jsm/loaders/GLTFLoader";
78

8-
import { getAttributeScale, getSkinnedVertex, parseAsBboxRatio } from "../utils";
9+
import { getAttributeScale, getSkinnedVertex, parseAsBboxRatio, isString } from "../utils";
10+
import { VARIANT_EXTENSION } from "../const/internal";
911

1012
import { Annotation } from "../annotation";
1113
import { AUTO } from "../const/external";
@@ -16,10 +18,12 @@ import { AUTO } from "../const/external";
1618
class Model {
1719
private _src: string;
1820
private _scene: THREE.Group;
21+
private _parser: GLTFParser | null;
1922
private _bbox: THREE.Box3;
2023
private _center: THREE.Vector3;
2124
private _animations: THREE.AnimationClip[];
2225
private _annotations: Annotation[];
26+
private _variants: Array<{ name: string }>;
2327
private _fixSkinnedBbox: boolean;
2428

2529
/**
@@ -96,17 +100,21 @@ class Model {
96100
src,
97101
scenes,
98102
center = AUTO,
103+
parser = null,
99104
animations = [],
100105
annotations = [],
106+
variants = [],
101107
fixSkinnedBbox = false,
102108
castShadow = true,
103109
receiveShadow = false
104110
}: {
105111
src: string;
106112
scenes: THREE.Object3D[];
107113
center?: typeof AUTO | Array<number | string>;
114+
parser?: GLTFParser | null,
108115
animations?: THREE.AnimationClip[];
109116
annotations?: Annotation[];
117+
variants?: Array<{ name: string }>;
110118
fixSkinnedBbox?: boolean;
111119
castShadow?: boolean;
112120
receiveShadow?: boolean;
@@ -116,9 +124,11 @@ class Model {
116124
const scene = new THREE.Group();
117125
scene.add(...scenes);
118126

127+
this._scene = scene;
128+
this._parser = parser;
119129
this._animations = animations;
120130
this._annotations = annotations;
121-
this._scene = scene;
131+
this._variants = variants;
122132
const bbox = this._getInitialBbox(fixSkinnedBbox);
123133

124134
// Move to position where bbox.min.y = 0
@@ -136,6 +146,52 @@ class Model {
136146
this.receiveShadow = receiveShadow;
137147
}
138148

149+
public async selectVariant(variant: number | string | null) {
150+
const variants = this._variants;
151+
const parser = this._parser;
152+
153+
if (variants.length <= 0 || !parser) return;
154+
155+
let variantIndex = 0;
156+
if (variant != null) {
157+
if (isString(variant)) {
158+
variantIndex = variants.findIndex(({ name }) => name === variant);
159+
} else {
160+
variantIndex = variant
161+
}
162+
}
163+
164+
const scene = this._scene;
165+
const matLoadPromises: Promise<any>[] = [];
166+
167+
scene.traverse(async (obj: THREE.Mesh) => {
168+
if (!obj.isMesh || !obj.userData.gltfExtensions) return;
169+
170+
const meshVariantDef = obj.userData.gltfExtensions[VARIANT_EXTENSION];
171+
172+
if (!meshVariantDef) return;
173+
174+
if (!obj.userData.originalMaterial) {
175+
obj.userData.originalMaterial = obj.material;
176+
}
177+
178+
const mapping = meshVariantDef.mappings
179+
.find(mapping => mapping.variants.includes(variantIndex));
180+
181+
if (mapping) {
182+
const loadMat = parser.getDependency("material", mapping.material);
183+
matLoadPromises.push(loadMat);
184+
185+
obj.material = await loadMat;
186+
parser.assignFinalMaterial(obj);
187+
} else {
188+
obj.material = obj.userData.originalMaterial;
189+
}
190+
});
191+
192+
return Promise.all(matLoadPromises);
193+
}
194+
139195
/**
140196
* Executes a user-supplied "reducer" callback function on each vertex of the model, in order, passing in the return value from the calculation on the preceding element.
141197
*/

packages/view3d/src/loader/GLTFLoader.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader";
1111
import View3D from "../View3D";
1212
import Model from "../core/Model";
1313
import Annotation from "../annotation/Annotation";
14-
import { ANNOTATION_EXTRA, CUSTOM_TEXTURE_LOD_EXTENSION, STANDARD_MAPS, TEXTURE_LOD_EXTRA } from "../const/internal";
14+
import { ANNOTATION_EXTRA, CUSTOM_TEXTURE_LOD_EXTENSION, STANDARD_MAPS, TEXTURE_LOD_EXTRA, VARIANT_EXTENSION } from "../const/internal";
1515
import { createLoadingContext } from "../utils";
1616

1717
import Loader from "./Loader";
@@ -84,8 +84,8 @@ class GLTFLoader extends Loader {
8484

8585
return new Promise((resolve, reject) => {
8686
try {
87-
loader.load(url, gltf => {
88-
const model = this._parseToModel(gltf, url);
87+
loader.load(url, async gltf => {
88+
const model = await this._parseToModel(gltf, url);
8989
resolve(model);
9090
}, evt => this._onLoadingProgress(evt, url, loadingContext), err => {
9191
loadingContext.initialized = true;
@@ -161,8 +161,8 @@ class GLTFLoader extends Loader {
161161
const loadingContext = createLoadingContext(view3D, gltfURL);
162162

163163
loader.manager = manager;
164-
loader.load(gltfURL, gltf => {
165-
const model = this._parseToModel(gltf, gltfFile.name);
164+
loader.load(gltfURL, async gltf => {
165+
const model = await this._parseToModel(gltf, gltfFile.name);
166166
revokeURLs();
167167
resolve(model);
168168
}, evt => this._onLoadingProgress(evt, gltfURL, loadingContext), err => {
@@ -173,7 +173,7 @@ class GLTFLoader extends Loader {
173173
});
174174
}
175175

176-
private _parseToModel(gltf: GLTF, src: string): Model {
176+
private async _parseToModel(gltf: GLTF, src: string): Promise<Model> {
177177
const view3D = this._view3D;
178178
const fixSkinnedBbox = view3D.fixSkinnedBbox;
179179

@@ -267,15 +267,26 @@ class GLTFLoader extends Loader {
267267
annotations.push(...view3D.annotation.parse(data));
268268
}
269269

270+
const userData = gltf.userData ?? {};
271+
const extensions = userData.gltfExtensions ?? {};
272+
const variants = extensions[VARIANT_EXTENSION] ? extensions[VARIANT_EXTENSION].variants : [];
273+
270274
const model = new Model({
271275
src,
272276
scenes: gltf.scenes,
273277
center: view3D.center,
274278
annotations,
279+
parser: gltf.parser,
275280
animations: gltf.animations,
281+
annotations,
282+
variants,
276283
fixSkinnedBbox
277284
});
278285

286+
if (view3D.variant) {
287+
await model.selectVariant(view3D.variant);
288+
}
289+
279290
return model;
280291
}
281292
}

0 commit comments

Comments
 (0)