@egjs/view3d
Version:
Fast & Customizable glTF 3D model viewer, packed with full of features!
435 lines (333 loc) • 12.8 kB
text/typescript
/*
* Copyright (c) 2020 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import * as THREE from "three";
import View3D from "./View3D";
import View3DError from "./core/View3DError";
import ERROR from "./const/error";
import { NoBoolean, TypedArray } from "./type/utils";
import { Model } from "./core";
export const isNumber = (val: any): val is number => typeof val === "number";
export const isString = (val: any): val is string => typeof val === "string";
export const isElement = (val: any): val is Element => !!val && val.nodeType === Node.ELEMENT_NODE;
export const getNullableElement = (el: HTMLElement | string | null, parent?: HTMLElement): HTMLElement | null => {
let targetEl: HTMLElement | null = null;
if (isString(el)) {
const parentEl = parent ? parent : document;
const queryResult = parentEl.querySelector(el);
if (!queryResult) {
return null;
}
targetEl = queryResult as HTMLElement;
} else if (isElement(el)) {
targetEl = el;
}
return targetEl;
};
export const getElement = (el: HTMLElement | string, parent?: HTMLElement): HTMLElement => {
const targetEl = getNullableElement(el, parent);
if (!targetEl) {
if (isString(el)) {
throw new View3DError(ERROR.MESSAGES.ELEMENT_NOT_FOUND(el), ERROR.CODES.ELEMENT_NOT_FOUND);
} else {
throw new View3DError(ERROR.MESSAGES.WRONG_TYPE(el, ["HTMLElement", "string"]), ERROR.CODES.WRONG_TYPE);
}
}
return targetEl;
};
export const findCanvas = (root: HTMLElement, selector: string): HTMLCanvasElement => {
const canvas = root.querySelector(selector) as HTMLCanvasElement;
if (!canvas) {
throw new View3DError(ERROR.MESSAGES.CANVAS_NOT_FOUND, ERROR.CODES.CANVAS_NOT_FOUND);
}
return canvas;
};
export const isCSSSelector = (val: any) => {
if (!isString(val)) return false;
const dummyEl = document.createDocumentFragment();
try {
dummyEl.querySelector(val);
} catch {
return false;
}
return true;
};
export const range = (end: number): number[] => {
if (!end || end <= 0) {
return [];
}
return Array.apply(0, Array(end)).map((undef, idx) => idx);
};
export const toRadian = (x: number) => x * Math.PI / 180;
export const toDegree = (x: number) => x * 180 / Math.PI;
export const clamp = (x: number, min: number, max: number) => Math.max(Math.min(x, max), min);
// Linear interpolation between a and b
export const lerp = (a: number, b: number, t: number) => {
return a * (1 - t) + b * t;
};
export const circulate = (val: number, min: number, max: number) => {
const size = Math.abs(max - min);
if (val < min) {
const offset = (min - val) % size;
val = max - offset;
} else if (val > max) {
const offset = (val - max) % size;
val = min + offset;
}
return val;
};
// eslint-disable-next-line @typescript-eslint/ban-types
export const merge = (target: object, ...srcs: object[]): object => {
srcs.forEach(source => {
Object.keys(source).forEach(key => {
const value = source[key];
if (Array.isArray(target[key]) && Array.isArray(value)) {
target[key] = [...target[key], ...value];
} else {
target[key] = value;
}
});
});
return target;
};
export const getBoxPoints = (box: THREE.Box3) => {
return [
box.min.clone(),
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
box.max.clone()
];
};
export const toPowerOfTwo = (val: number) => {
let result = 1;
while (result < val) {
result *= 2;
}
return result;
};
export const getPrimaryAxisIndex = (basis: THREE.Vector3[], viewDir: THREE.Vector3) => {
let primaryIdx = 0;
let maxDot = 0;
basis.forEach((axes, axesIdx) => {
const dotProduct = Math.abs(viewDir.dot(axes));
if (dotProduct > maxDot) {
primaryIdx = axesIdx;
maxDot = dotProduct;
}
});
return primaryIdx;
};
// In radian
export const getRotationAngle = (center: THREE.Vector2, v1: THREE.Vector2, v2: THREE.Vector2) => {
const centerToV1 = new THREE.Vector2().subVectors(v1, center).normalize();
const centerToV2 = new THREE.Vector2().subVectors(v2, center).normalize();
// Get the rotation angle with the model's NDC coordinates as the center.
const deg = centerToV2.angle() - centerToV1.angle();
const compDeg = -Math.sign(deg) * (2 * Math.PI - Math.abs(deg));
// Take the smaller deg
const rotationAngle = Math.abs(deg) < Math.abs(compDeg) ? deg : compDeg;
return rotationAngle;
};
export const getObjectOption = <T extends boolean | Partial<object>>(val: T): NoBoolean<T> => typeof val === "object" ? val : {} as any;
export const toBooleanString = (val: boolean) => val ? "true" : "false";
export const getRotatedPosition = (distance: number, yawDeg: number, pitchDeg: number) => {
const yaw = toRadian(yawDeg);
const pitch = toRadian(pitchDeg);
const newPos = new THREE.Vector3(0, 0, 0);
newPos.y = distance * Math.sin(pitch);
newPos.z = distance * Math.cos(pitch);
newPos.x = newPos.z * Math.sin(-yaw);
newPos.z = newPos.z * Math.cos(-yaw);
return newPos;
};
// In Radians
export const directionToYawPitch = (direction: THREE.Vector3) => {
const xz = new THREE.Vector2(direction.x, direction.z);
const origin = new THREE.Vector2();
const yaw = Math.abs(direction.y) <= 0.99
? getRotationAngle(origin, new THREE.Vector2(0, 1), xz)
: 0;
const pitch = Math.atan2(direction.y, xz.distanceTo(origin));
return {
yaw,
pitch
};
};
export const createLoadingContext = (view3D: View3D, src: string): View3D["loadingContext"][0] => {
const context = {
src,
loaded: 0,
total: 0,
lengthComputable: false,
initialized: false
};
view3D.loadingContext.push(context);
return context;
};
export const getAttributeScale = (attrib: THREE.BufferAttribute | THREE.InterleavedBufferAttribute) => {
if (attrib.normalized && ArrayBuffer.isView(attrib.array)) {
const buffer = attrib.array as TypedArray;
const isSigned = isSignedArrayBuffer(buffer);
const scale = 1 / (Math.pow(2, 8 * buffer.BYTES_PER_ELEMENT) - 1);
return isSigned ? scale * 2 : scale;
} else {
return 1;
}
};
export const getSkinnedVertex = (posIdx: number, mesh: THREE.SkinnedMesh, positionScale: number, skinWeightScale: number) => {
const geometry = mesh.geometry;
const positions = geometry.attributes.position;
const skinIndicies = geometry.attributes.skinIndex;
const skinWeights = geometry.attributes.skinWeight;
const skeleton = mesh.skeleton;
const boneMatricies = skeleton.boneMatrices;
const pos = new THREE.Vector3().fromBufferAttribute(positions, posIdx).multiplyScalar(positionScale);
const skinned = new THREE.Vector4(0, 0, 0, 0);
const skinVertex = new THREE.Vector4(pos.x, pos.y, pos.z).applyMatrix4(mesh.bindMatrix);
const weights = [
skinWeights.getX(posIdx),
skinWeights.getY(posIdx),
skinWeights.getZ(posIdx),
skinWeights.getW(posIdx)
].map(weight => weight * skinWeightScale);
const indicies = [
skinIndicies.getX(posIdx),
skinIndicies.getY(posIdx),
skinIndicies.getZ(posIdx),
skinIndicies.getW(posIdx)
];
weights.forEach((weight, index) => {
const boneMatrix = new THREE.Matrix4().fromArray(boneMatricies, indicies[index] * 16);
skinned.add(skinVertex.clone().applyMatrix4(boneMatrix).multiplyScalar(weight));
});
const transformed = new THREE.Vector3().fromArray(skinned.applyMatrix4(mesh.bindMatrixInverse).toArray());
transformed.applyMatrix4(mesh.matrixWorld);
return transformed;
};
export const isSignedArrayBuffer = (buffer: TypedArray) => {
const testBuffer = new (buffer.constructor as any)(1);
testBuffer[0] = -1;
return testBuffer[0] < 0;
};
export const checkHalfFloatAvailable = (renderer: THREE.WebGLRenderer) => {
if (renderer.capabilities.isWebGL2) {
return true;
} else {
const gl = renderer.getContext();
const texture = gl.createTexture();
let available = true;
try {
const data = new Uint16Array(4);
const ext = gl.getExtension("OES_texture_half_float");
if (!ext) {
available = false;
} else {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, ext.HALF_FLOAT_OES, data);
const err = gl.getError();
available = err === gl.NO_ERROR;
}
} catch (err) {
available = false;
}
gl.deleteTexture(texture);
return available;
}
};
export const getFaceVertices = (model: Model | null, meshIndex: number, faceIndex: number): THREE.Vector3[] | null => {
if (!model || meshIndex < 0 || faceIndex < 0) return null;
const mesh = model.meshes[meshIndex];
const indexes = mesh?.geometry.index?.array;
const face = indexes
? range(3).map(idx => indexes[3 * faceIndex + idx])
: null;
if (!mesh || !indexes || !face || face.some(val => val == null)) return null;
const position = mesh.geometry.getAttribute("position");
const vertices = face.map((index: number) => {
return new THREE.Vector3().fromBufferAttribute(position, index);
});
return vertices;
};
export const getAnimatedFace = (model: Model | null, meshIndex: number, faceIndex: number): THREE.Vector3[] | null => {
const vertices = getFaceVertices(model, meshIndex, faceIndex);
if (!vertices) return null;
const mesh = model!.meshes[meshIndex];
const indexes = mesh.geometry.getIndex()!;
const face = (indexes.array as TypedArray).slice(3 * faceIndex, 3 * faceIndex + 3);
if ((mesh as THREE.SkinnedMesh).isSkinnedMesh) {
const geometry = mesh.geometry;
const positions = geometry.attributes.position;
const skinWeights = geometry.attributes.skinWeight;
const positionScale = getAttributeScale(positions);
const skinWeightScale = getAttributeScale(skinWeights);
vertices.forEach((vertex, idx) => {
const posIdx = face[idx];
const transformed = getSkinnedVertex(posIdx, mesh as THREE.SkinnedMesh, positionScale, skinWeightScale);
vertex.copy(transformed);
});
} else {
vertices.forEach(vertex => {
vertex.applyMatrix4(mesh.matrixWorld);
});
}
return vertices;
};
export const subclip = (sourceClip: THREE.AnimationClip, name: string, startTime: number, endTime: number) => {
const clip = sourceClip.clone();
clip.name = name;
const tracks: THREE.KeyframeTrack[] = [];
clip.tracks.forEach(track => {
const valueSize = track.getValueSize();
const times: number[] = [];
const values: number[] = [];
for (let timeIdx = 0; timeIdx < track.times.length; ++timeIdx) {
const time = track.times[timeIdx];
const nextTime = track.times[timeIdx + 1];
const prevTime = track.times[timeIdx - 1];
const isPrevFrame = nextTime && time < startTime && nextTime > startTime;
const isMiddleFrame = time >= startTime && time < endTime;
const isNextFrame = prevTime && time >= endTime && prevTime < endTime;
if (!isPrevFrame && !isMiddleFrame && !isNextFrame) continue;
times.push(time);
for (let k = 0; k < valueSize; ++k) {
values.push(track.values[timeIdx * valueSize + k]);
}
}
if (times.length === 0) return;
track.times = convertArray(times, track.times.constructor);
track.values = convertArray(values, track.values.constructor);
tracks.push(track);
});
clip.tracks = tracks;
for (let i = 0; i < clip.tracks.length; ++i) {
clip.tracks[i].shift(-startTime);
}
clip.duration = endTime - startTime;
return clip;
};
// From three.js AnimationUtils
// https://github.com/mrdoob/three.js/blob/68daccedef9c9c325cc5f4c929fcaf05229aa1b3/src/animation/AnimationUtils.js#L20
// The MIT License
// Copyright © 2010-2022 three.js authors
const convertArray = (array, type, forceClone = false) => {
if (!array || // let 'undefined' and 'null' pass
!forceClone && array.constructor === type ) return array;
if (typeof type.BYTES_PER_ELEMENT === "number") {
return new type( array ); // create typed array
}
return Array.prototype.slice.call(array); // create Array
};
export const parseAsBboxRatio = (arr: Array<number | string>, bbox: THREE.Box3) => {
const min = bbox.min.toArray();
const size = new THREE.Vector3().subVectors(bbox.max, bbox.min).toArray();
return new THREE.Vector3().fromArray(arr.map((val, idx) => {
if (!isString(val)) return val;
const ratio = parseFloat(val) * 0.01;
return min[idx] + ratio * size[idx];
}));
};