@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
698 lines (599 loc) • 20.7 kB
text/typescript
/* @license
* Copyright 2020 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Color, ColorRepresentation, DoubleSide, FrontSide, MeshPhysicalMaterial, Vector2} from 'three';
import {AlphaMode, RGB} from '../../three-components/gltf-instance/gltf-2.0.js';
import {Material as MaterialInterface} from './api.js';
import {LazyLoader, VariantData} from './model.js';
import {PBRMetallicRoughness} from './pbr-metallic-roughness.js';
import {TextureInfo, TextureUsage} from './texture-info.js';
import {$correlatedObjects, $onUpdate, ThreeDOMElement} from './three-dom-element.js';
const $pbrMetallicRoughness = Symbol('pbrMetallicRoughness');
const $normalTexture = Symbol('normalTexture');
const $occlusionTexture = Symbol('occlusionTexture');
const $emissiveTexture = Symbol('emissiveTexture');
const $backingThreeMaterial = Symbol('backingThreeMaterial');
const $applyAlphaCutoff = Symbol('applyAlphaCutoff');
const $getAlphaMode = Symbol('getAlphaMode');
export const $lazyLoadGLTFInfo = Symbol('lazyLoadGLTFInfo');
const $initialize = Symbol('initialize');
export const $getLoadedMaterial = Symbol('getLoadedMaterial');
export const $ensureMaterialIsLoaded = Symbol('ensureMaterialIsLoaded');
export const $gltfIndex = Symbol('gltfIndex');
export const $setActive = Symbol('setActive');
export const $variantIndices = Symbol('variantIndices');
const $isActive = Symbol('isActive');
export const $variantSet = Symbol('variantSet');
const $modelVariants = Symbol('modelVariants');
const $name = Symbol('name');
const $pbrTextures = Symbol('pbrTextures');
/**
* Material facade implementation for Three.js materials
*/
export class Material extends ThreeDOMElement implements MaterialInterface {
private[$pbrMetallicRoughness]!: PBRMetallicRoughness;
private[$normalTexture]!: TextureInfo;
private[$occlusionTexture]!: TextureInfo;
private[$emissiveTexture]!: TextureInfo;
private[$lazyLoadGLTFInfo]?: LazyLoader;
private[$gltfIndex]: number;
private[$isActive]: boolean;
private[$variantSet] = new Set<number>();
private[$name]?: string;
readonly[$modelVariants]: Map<string, VariantData>;
private[$pbrTextures] = new Map<TextureUsage, TextureInfo>();
get[$backingThreeMaterial](): MeshPhysicalMaterial {
return (this[$correlatedObjects] as Set<MeshPhysicalMaterial>)
.values()
.next()
.value;
}
constructor(
onUpdate: () => void,
gltfIndex: number,
isActive: boolean,
modelVariants: Map<string, VariantData>,
correlatedMaterials: Set<MeshPhysicalMaterial>,
name: string|undefined,
lazyLoadInfo: LazyLoader|undefined = undefined,
) {
super(onUpdate, correlatedMaterials);
this[$gltfIndex] = gltfIndex;
this[$isActive] = isActive;
this[$modelVariants] = modelVariants;
this[$name] = name;
if (lazyLoadInfo == null) {
this[$initialize]();
} else {
this[$lazyLoadGLTFInfo] = lazyLoadInfo;
}
}
private[$initialize](): void {
const onUpdate = this[$onUpdate] as () => void;
const correlatedMaterials =
this[$correlatedObjects] as Set<MeshPhysicalMaterial>;
this[$pbrMetallicRoughness] =
new PBRMetallicRoughness(onUpdate, correlatedMaterials);
const {normalMap, aoMap, emissiveMap} =
correlatedMaterials.values().next().value;
this[$normalTexture] = new TextureInfo(
onUpdate,
TextureUsage.Normal,
normalMap,
correlatedMaterials,
);
this[$occlusionTexture] = new TextureInfo(
onUpdate,
TextureUsage.Occlusion,
aoMap,
correlatedMaterials,
);
this[$emissiveTexture] = new TextureInfo(
onUpdate,
TextureUsage.Emissive,
emissiveMap,
correlatedMaterials,
);
const createTextureInfo = (usage: TextureUsage) => {
this[$pbrTextures].set(
usage,
new TextureInfo(
onUpdate,
usage,
null,
correlatedMaterials,
));
};
createTextureInfo(TextureUsage.Clearcoat);
createTextureInfo(TextureUsage.ClearcoatRoughness);
createTextureInfo(TextureUsage.ClearcoatNormal);
createTextureInfo(TextureUsage.SheenColor);
createTextureInfo(TextureUsage.SheenRoughness);
createTextureInfo(TextureUsage.Transmission);
createTextureInfo(TextureUsage.Thickness);
createTextureInfo(TextureUsage.Specular);
createTextureInfo(TextureUsage.SpecularColor);
createTextureInfo(TextureUsage.Iridescence);
createTextureInfo(TextureUsage.IridescenceThickness);
createTextureInfo(TextureUsage.Anisotropy);
}
async[$getLoadedMaterial](): Promise<MeshPhysicalMaterial> {
if (this[$lazyLoadGLTFInfo] != null) {
const {set, material} = await this[$lazyLoadGLTFInfo]!.doLazyLoad();
// Fills in the missing data.
this[$correlatedObjects] = set as Set<MeshPhysicalMaterial>;
this[$initialize]();
// Releases lazy load info.
this[$lazyLoadGLTFInfo] = undefined;
// Redefines the method as a noop method.
this.ensureLoaded = async () => {};
return material as MeshPhysicalMaterial;
}
return this[$correlatedObjects]!.values().next().value;
}
private colorFromRgb(rgb: RGB|string): Color {
const color = new Color();
if (rgb instanceof Array) {
color.fromArray(rgb);
} else {
color.set(rgb as ColorRepresentation);
}
return color;
}
[$ensureMaterialIsLoaded]() {
if (this[$lazyLoadGLTFInfo] == null) {
return;
}
throw new Error(`Material "${this.name}" has not been loaded, call 'await
myMaterial.ensureLoaded()' before using an unloaded material.`);
}
async ensureLoaded() {
await this[$getLoadedMaterial]();
}
get isLoaded() {
return this[$lazyLoadGLTFInfo] == null;
}
get isActive(): boolean {
return this[$isActive];
}
[$setActive](isActive: boolean) {
this[$isActive] = isActive;
}
get name(): string {
return this[$name] || '';
}
set name(name: string) {
this[$name] = name;
if (this[$correlatedObjects] != null) {
for (const threeMaterial of this[$correlatedObjects]!) {
threeMaterial.name = name;
}
}
}
get pbrMetallicRoughness(): PBRMetallicRoughness {
this[$ensureMaterialIsLoaded]();
return this[$pbrMetallicRoughness];
}
get normalTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$normalTexture];
}
get occlusionTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$occlusionTexture];
}
get emissiveTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$emissiveTexture];
}
get emissiveFactor(): RGB {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial].emissive.toArray() as RGB);
}
get index(): number {
return this[$gltfIndex];
}
[$variantIndices]() {
return this[$variantSet];
}
hasVariant(name: string): boolean {
const variantData = this[$modelVariants].get(name);
return variantData != null && this[$variantSet].has(variantData.index);
}
setEmissiveFactor(rgb: RGB|string) {
this[$ensureMaterialIsLoaded]();
const color = this.colorFromRgb(rgb);
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.emissive.set(color);
}
this[$onUpdate]();
}
[$getAlphaMode](): string {
// Follows implementation of GLTFExporter from three.js
if (this[$backingThreeMaterial].transparent) {
return 'BLEND';
} else {
if (this[$backingThreeMaterial].alphaTest > 0.0) {
return 'MASK';
}
}
return 'OPAQUE';
}
[$applyAlphaCutoff]() {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
if (this[$getAlphaMode]() === 'MASK') {
if (material.alphaTest == undefined) {
material.alphaTest = 0.5;
}
} else {
(material.alphaTest as number | undefined) = undefined;
}
material.needsUpdate = true;
}
}
setAlphaCutoff(cutoff: number): void {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.alphaTest = cutoff;
material.needsUpdate = true;
}
// Set AlphaCutoff to undefined if AlphaMode is not MASK.
this[$applyAlphaCutoff]();
this[$onUpdate]();
}
getAlphaCutoff(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].alphaTest;
}
setDoubleSided(doubleSided: boolean): void {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
// When double-sided is disabled gltf spec dictates that Back-Face culling
// must be disabled, in three.js parlance that would mean FrontSide
// rendering only.
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#double-sided
material.side = doubleSided ? DoubleSide : FrontSide;
material.needsUpdate = true;
}
this[$onUpdate]();
}
getDoubleSided(): boolean {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].side == DoubleSide;
}
setAlphaMode(alphaMode: AlphaMode): void {
this[$ensureMaterialIsLoaded]();
const enableTransparency =
(material: MeshPhysicalMaterial, enabled: boolean): void => {
material.transparent = enabled;
material.depthWrite = !enabled;
};
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
enableTransparency(material, alphaMode === 'BLEND');
if (alphaMode === 'MASK') {
material.alphaTest = 0.5;
} else {
(material.alphaTest as number | undefined) = undefined;
}
material.needsUpdate = true;
}
this[$onUpdate]();
}
getAlphaMode(): AlphaMode {
this[$ensureMaterialIsLoaded]();
return (this[$getAlphaMode]() as AlphaMode);
}
/**
* PBR Next properties.
*/
// KHR_materials_emissive_strength
get emissiveStrength(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].emissiveIntensity;
}
setEmissiveStrength(emissiveStrength: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.emissiveIntensity = emissiveStrength;
}
this[$onUpdate]();
}
// KHR_materials_clearcoat
get clearcoatFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].clearcoat;
}
get clearcoatRoughnessFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].clearcoatRoughness;
}
get clearcoatTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Clearcoat)!;
}
get clearcoatRoughnessTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.ClearcoatRoughness)!;
}
get clearcoatNormalTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.ClearcoatNormal)!;
}
get clearcoatNormalScale(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].clearcoatNormalScale.x;
}
setClearcoatFactor(clearcoatFactor: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.clearcoat = clearcoatFactor;
}
this[$onUpdate]();
}
setClearcoatRoughnessFactor(clearcoatRoughnessFactor: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.clearcoatRoughness = clearcoatRoughnessFactor;
}
this[$onUpdate]();
}
setClearcoatNormalScale(clearcoatNormalScale: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.clearcoatNormalScale =
new Vector2(clearcoatNormalScale, clearcoatNormalScale);
}
this[$onUpdate]();
}
// KHR_materials_ior
get ior(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].ior;
}
setIor(ior: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.ior = ior;
}
this[$onUpdate]();
}
// KHR_materials_sheen
get sheenColorFactor(): RGB {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial].sheenColor.toArray() as RGB);
}
get sheenColorTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.SheenColor)!;
}
get sheenRoughnessFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].sheenRoughness;
}
get sheenRoughnessTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.SheenRoughness)!;
}
setSheenColorFactor(rgb: RGB|string) {
this[$ensureMaterialIsLoaded]();
const color = this.colorFromRgb(rgb);
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.sheenColor.set(color);
// Three.js GLTFExporter checks for internal sheen value.
material.sheen = 1;
}
this[$onUpdate]();
}
setSheenRoughnessFactor(roughness: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.sheenRoughness = roughness;
// Three.js GLTFExporter checks for internal sheen value.
material.sheen = 1;
}
this[$onUpdate]();
}
// KHR_materials_transmission
get transmissionFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].transmission;
}
get transmissionTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Transmission)!;
}
setTransmissionFactor(transmission: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.transmission = transmission;
}
this[$onUpdate]();
}
// KHR_materials_volume
get thicknessFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].thickness;
}
get thicknessTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Thickness)!;
}
get attenuationDistance(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].attenuationDistance;
}
get attenuationColor(): RGB {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial].attenuationColor.toArray() as RGB);
}
setThicknessFactor(thickness: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.thickness = thickness;
}
this[$onUpdate]();
}
setAttenuationDistance(attenuationDistance: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.attenuationDistance = attenuationDistance;
}
this[$onUpdate]();
}
setAttenuationColor(rgb: RGB|string) {
this[$ensureMaterialIsLoaded]();
const color = this.colorFromRgb(rgb);
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.attenuationColor.set(color);
}
this[$onUpdate]();
}
// KHR_materials_specular
get specularFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].specularIntensity;
}
get specularTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Specular)!;
}
get specularColorFactor(): RGB {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial].specularColor.toArray() as RGB);
}
get specularColorTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.SheenColor)!;
}
setSpecularFactor(specularFactor: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.specularIntensity = specularFactor;
}
this[$onUpdate]();
}
setSpecularColorFactor(rgb: RGB|string) {
this[$ensureMaterialIsLoaded]();
const color = this.colorFromRgb(rgb);
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.specularColor.set(color);
}
this[$onUpdate]();
}
// KHR_materials_iridescence
get iridescenceFactor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].iridescence;
}
get iridescenceTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Iridescence)!;
}
get iridescenceIor(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].iridescenceIOR;
}
get iridescenceThicknessMinimum(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].iridescenceThicknessRange[0];
}
get iridescenceThicknessMaximum(): number {
this[$ensureMaterialIsLoaded]();
return this[$backingThreeMaterial].iridescenceThicknessRange[1];
}
get iridescenceThicknessTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.IridescenceThickness)!;
}
setIridescenceFactor(iridescence: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.iridescence = iridescence;
}
this[$onUpdate]();
}
setIridescenceIor(ior: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.iridescenceIOR = ior;
}
this[$onUpdate]();
}
setIridescenceThicknessMinimum(thicknessMin: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.iridescenceThicknessRange[0] = thicknessMin;
}
this[$onUpdate]();
}
setIridescenceThicknessMaximum(thicknessMax: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
material.iridescenceThicknessRange[1] = thicknessMax;
}
this[$onUpdate]();
}
// KHR_materials_anisotropy
get anisotropyStrength(): number {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial] as any).anisotropy;
}
get anisotropyRotation(): number {
this[$ensureMaterialIsLoaded]();
return (this[$backingThreeMaterial] as any).anisotropyRotation;
}
get anisotropyTexture(): TextureInfo {
this[$ensureMaterialIsLoaded]();
return this[$pbrTextures].get(TextureUsage.Anisotropy)!;
}
setAnisotropyStrength(strength: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
(material as any).anisotropy = strength;
}
this[$onUpdate]();
}
setAnisotropyRotation(rotation: number) {
this[$ensureMaterialIsLoaded]();
for (const material of this[$correlatedObjects] as
Set<MeshPhysicalMaterial>) {
(material as any).anisotropyRotation = rotation;
}
this[$onUpdate]();
}
}