mirror of
https://github.com/pissang/claygl.git
synced 2026-02-01 17:27:08 +00:00
refact: provide App3D instead of option based application.create
This commit is contained in:
parent
391481f713
commit
30cbd2f7b2
1287
src/App3D.ts
1287
src/App3D.ts
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ import type Camera from './Camera';
|
||||
import type Renderable from './Renderable';
|
||||
import type Vector2 from './math/Vector2';
|
||||
import type BoundingBox from './math/BoundingBox';
|
||||
import type { Intersection } from './picking/RayPicking';
|
||||
|
||||
export type AttributeType = 'byte' | 'ubyte' | 'short' | 'ushort' | 'float';
|
||||
export type AttributeSize = 1 | 2 | 3 | 4;
|
||||
@ -324,7 +325,7 @@ class GeometryBase {
|
||||
renderer: Renderer,
|
||||
camera: Camera,
|
||||
renderable: Renderable,
|
||||
out: Vector2
|
||||
out: Intersection[]
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
@ -335,7 +336,7 @@ class GeometryBase {
|
||||
* ```
|
||||
* @type {?Function}
|
||||
*/
|
||||
pickByRay?: (ray: Ray, renderable: Renderable, out: Vector2) => boolean;
|
||||
pickByRay?: (ray: Ray, renderable: Renderable, out: Intersection[]) => boolean;
|
||||
|
||||
protected _cache = new ClayCache();
|
||||
private _attributeList: string[];
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import * as util from './core/util';
|
||||
import * as colorUtil from './core/color';
|
||||
import Shader, { ShaderDefineValue, ShaderPrecision, ShaderType, ShaderUniform } from './Shader';
|
||||
import Texture from './Texture';
|
||||
|
||||
type MaterialUniformValue = number | string | ArrayLike<number> | Texture;
|
||||
|
||||
const parseColor = colorUtil.parseToFloat;
|
||||
|
||||
const programKeyCache: Record<string, string> = {};
|
||||
@ -150,7 +154,7 @@ class Material {
|
||||
* @param {string} symbol
|
||||
* @param {number|array|clay.Texture} value
|
||||
*/
|
||||
setUniform(symbol: string, value: number | string | ArrayLike<number>) {
|
||||
setUniform(symbol: string, value: MaterialUniformValue) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
// console.warn('Uniform value "' + symbol + '" is undefined');
|
||||
|
||||
@ -55,6 +55,11 @@ export interface ClayNodeOpts {
|
||||
* If node and its chilren invisible
|
||||
*/
|
||||
invisible?: boolean;
|
||||
|
||||
/**
|
||||
* If not trigger event. Available in the App3D
|
||||
*/
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
interface ClayNode extends ClayNodeOpts {
|
||||
|
||||
@ -195,7 +195,7 @@ interface Viewport {
|
||||
height: number;
|
||||
devicePixelRatio: number;
|
||||
}
|
||||
interface RendererOpts {
|
||||
export interface RendererOpts {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
|
||||
/**
|
||||
|
||||
@ -64,7 +64,7 @@ function setUniforms(
|
||||
}
|
||||
}
|
||||
|
||||
class RenderList {
|
||||
export class RenderList {
|
||||
opaque: Renderable[] = [];
|
||||
transparent: Renderable[] = [];
|
||||
|
||||
@ -113,6 +113,8 @@ class Scene extends ClayNode {
|
||||
// Uniforms for shadow map.
|
||||
shadowUniforms: Record<string, ShaderUniform> = {};
|
||||
|
||||
skybox?: Skybox;
|
||||
|
||||
private _cameraList: Camera[] = [];
|
||||
|
||||
// Properties to save the light information in the scene
|
||||
@ -230,8 +232,6 @@ class Scene extends ClayNode {
|
||||
* Clone a node and it's children, including mesh, camera, light, etc.
|
||||
* Unlike using `Node#clone`. It will clone skeleton and remap the joints. Material will also be cloned.
|
||||
*
|
||||
* @param {clay.Node} node
|
||||
* @return {clay.Node}
|
||||
*/
|
||||
cloneNode(node: ClayNode) {
|
||||
const newNode = node.clone();
|
||||
|
||||
201
src/app/EventManager.ts
Normal file
201
src/app/EventManager.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import * as rayPicking from '../picking/rayPicking';
|
||||
import vendor from '../core/vendor';
|
||||
import type Renderer from '../Renderer';
|
||||
import type ClayNode from '../Node';
|
||||
import Scene from '../Scene';
|
||||
|
||||
// TODO Use pointer event
|
||||
const EVENT_NAMES = [
|
||||
'click',
|
||||
'dblclick',
|
||||
'mouseover',
|
||||
'mouseout',
|
||||
'mousemove',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'touchstart',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'mousewheel'
|
||||
] as const;
|
||||
|
||||
type ClayEventType =
|
||||
| 'click'
|
||||
| 'dblclick'
|
||||
| 'mousewheel'
|
||||
| 'pointerdown'
|
||||
| 'pointermove'
|
||||
| 'pointerup'
|
||||
| 'pointerover'
|
||||
| 'pointerout';
|
||||
|
||||
// TODO only mouseout event only have target property from Itersection
|
||||
export interface ClayMouseEvent extends Partial<rayPicking.Intersection> {
|
||||
target: ClayNode;
|
||||
type: ClayEventType;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
wheelDelta?: number;
|
||||
cancelBubble?: boolean;
|
||||
}
|
||||
|
||||
function packageEvent(
|
||||
eventType: ClayEventType,
|
||||
pickResult: Partial<rayPicking.Intersection>,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
wheelDelta?: number
|
||||
) {
|
||||
return Object.assign(
|
||||
{
|
||||
type: eventType,
|
||||
offsetX: offsetX,
|
||||
offsetY: offsetY,
|
||||
wheelDelta: wheelDelta
|
||||
},
|
||||
pickResult
|
||||
) as ClayMouseEvent;
|
||||
}
|
||||
|
||||
function bubblingEvent(target: ClayNode | undefined, event: ClayMouseEvent) {
|
||||
while (target && !event.cancelBubble) {
|
||||
target.trigger(event.type, event);
|
||||
target = target.getParent();
|
||||
}
|
||||
}
|
||||
|
||||
function makeHandlerName(eveType: string) {
|
||||
return '_' + eveType + 'Handler';
|
||||
}
|
||||
|
||||
export class EventManager {
|
||||
private _renderer: Renderer;
|
||||
private _container: HTMLElement;
|
||||
private _scene: Scene;
|
||||
constructor(container: HTMLElement, renderer: Renderer, scene: Scene) {
|
||||
this._container = container;
|
||||
this._renderer = renderer;
|
||||
this._scene = scene;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const dom = this._container;
|
||||
const scene = this._scene;
|
||||
const renderer = this._renderer;
|
||||
const mainCamera = scene.getMainCamera();
|
||||
|
||||
let oldTarget: ClayNode | undefined;
|
||||
EVENT_NAMES.forEach((domEveType) => {
|
||||
vendor.addEventListener(
|
||||
dom,
|
||||
domEveType,
|
||||
((this as any)[makeHandlerName(domEveType)] = (e: MouseEvent | TouchEvent) => {
|
||||
if (!mainCamera) {
|
||||
// Not have camera yet.
|
||||
return;
|
||||
}
|
||||
e.preventDefault && e.preventDefault();
|
||||
|
||||
const box = dom.getBoundingClientRect();
|
||||
let offsetX, offsetY;
|
||||
let eveType: ClayEventType;
|
||||
|
||||
if (domEveType.indexOf('touch') >= 0) {
|
||||
const touch =
|
||||
domEveType !== 'touchend'
|
||||
? (e as TouchEvent).targetTouches[0]
|
||||
: (e as TouchEvent).changedTouches[0];
|
||||
|
||||
offsetX = touch.clientX - box.left;
|
||||
offsetY = touch.clientY - box.top;
|
||||
} else {
|
||||
offsetX = (e as MouseEvent).clientX - box.left;
|
||||
offsetY = (e as MouseEvent).clientY - box.top;
|
||||
}
|
||||
|
||||
const pickResult = rayPicking.pick(renderer, scene, mainCamera, offsetX, offsetY);
|
||||
|
||||
let delta;
|
||||
if (domEveType === 'mousewheel') {
|
||||
delta = (e as any).wheelDelta ? (e as any).wheelDelta / 120 : -(e.detail || 0) / 3;
|
||||
}
|
||||
|
||||
if (pickResult) {
|
||||
// Just ignore silent element.
|
||||
if (pickResult.target.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (domEveType === 'mousemove' || domEveType === 'touchmove') {
|
||||
// PENDING touchdown should trigger mouseover event ?
|
||||
const targetChanged = pickResult.target !== oldTarget;
|
||||
if (targetChanged) {
|
||||
oldTarget &&
|
||||
bubblingEvent(
|
||||
oldTarget,
|
||||
packageEvent(
|
||||
'pointerout',
|
||||
{
|
||||
target: oldTarget
|
||||
},
|
||||
offsetX,
|
||||
offsetY
|
||||
)
|
||||
);
|
||||
}
|
||||
bubblingEvent(
|
||||
pickResult.target,
|
||||
packageEvent('pointermove', pickResult, offsetX, offsetY)
|
||||
);
|
||||
if (targetChanged) {
|
||||
bubblingEvent(
|
||||
pickResult.target,
|
||||
packageEvent('pointerover', pickResult, offsetX, offsetY)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Map events
|
||||
eveType =
|
||||
domEveType === 'mousedown' || domEveType === 'touchstart'
|
||||
? 'pointerdown'
|
||||
: domEveType === 'mouseup' || domEveType === 'touchend'
|
||||
? 'pointerup'
|
||||
: domEveType === 'mouseover'
|
||||
? 'pointerover'
|
||||
: domEveType === 'mouseout'
|
||||
? 'pointerout'
|
||||
: domEveType;
|
||||
bubblingEvent(
|
||||
pickResult.target,
|
||||
packageEvent(eveType, pickResult, offsetX, offsetY, delta)
|
||||
);
|
||||
}
|
||||
oldTarget = pickResult.target;
|
||||
} else if (oldTarget) {
|
||||
bubblingEvent(
|
||||
oldTarget,
|
||||
packageEvent(
|
||||
'pointerout',
|
||||
{
|
||||
target: oldTarget
|
||||
},
|
||||
offsetX,
|
||||
offsetY
|
||||
)
|
||||
);
|
||||
oldTarget = undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
EVENT_NAMES.forEach((eveType) => {
|
||||
const handler = (this as any)[makeHandlerName(eveType)];
|
||||
handler && vendor.removeEventListener(this._container, eveType, handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
111
src/app/GPUResourceManager.ts
Normal file
111
src/app/GPUResourceManager.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import type Geometry from '../Geometry';
|
||||
import type AmbientCubemap from '../light/AmbientCubemap';
|
||||
import type Material from '../Material';
|
||||
import type Renderer from '../Renderer';
|
||||
import type Scene from '../Scene';
|
||||
import type Texture from '../Texture';
|
||||
|
||||
type Resource = Geometry | Texture;
|
||||
const usedMap = new WeakMap<Resource, number>();
|
||||
|
||||
function markUnused(resourceList: Resource[]) {
|
||||
for (let i = 0; i < resourceList.length; i++) {
|
||||
usedMap.set(resourceList[i], 0);
|
||||
}
|
||||
}
|
||||
|
||||
function checkAndDispose(renderer: Renderer, resourceList: Resource[]) {
|
||||
for (let i = 0; i < resourceList.length; i++) {
|
||||
if (!usedMap.get(resourceList[i])) {
|
||||
resourceList[i].dispose(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateUsed(resource: Resource, list: Resource[]) {
|
||||
const used = (usedMap.get(resource) || 0) + 1;
|
||||
usedMap.set(resource, used);
|
||||
if (used === 1) {
|
||||
// Don't push to the list twice.
|
||||
list.push(resource);
|
||||
}
|
||||
}
|
||||
function collectResources(
|
||||
scene: Scene,
|
||||
textureResourceList: Texture[],
|
||||
geometryResourceList: Geometry[]
|
||||
) {
|
||||
let prevMaterial: Material;
|
||||
let prevGeometry: Geometry;
|
||||
scene.traverse(function (renderable) {
|
||||
if (renderable.isRenderable()) {
|
||||
const geometry = renderable.geometry;
|
||||
const material = renderable.material;
|
||||
|
||||
// TODO optimize!!
|
||||
if (material !== prevMaterial) {
|
||||
const textureUniforms = material.getTextureUniforms();
|
||||
for (let u = 0; u < textureUniforms.length; u++) {
|
||||
const uniformName = textureUniforms[u];
|
||||
const val = material.uniforms[uniformName].value;
|
||||
const uniformType = material.uniforms[uniformName].type;
|
||||
if (!val) {
|
||||
continue;
|
||||
}
|
||||
if (uniformType === 't') {
|
||||
updateUsed(val, textureResourceList);
|
||||
} else if (uniformType === 'tv') {
|
||||
for (let k = 0; k < val.length; k++) {
|
||||
if (val[k]) {
|
||||
updateUsed(val[k], textureResourceList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (geometry !== prevGeometry) {
|
||||
updateUsed(geometry, geometryResourceList);
|
||||
}
|
||||
|
||||
prevMaterial = material;
|
||||
prevGeometry = geometry;
|
||||
}
|
||||
});
|
||||
|
||||
for (let k = 0; k < scene.lights.length; k++) {
|
||||
const cubemap = (scene.lights[k] as AmbientCubemap).cubemap;
|
||||
// Track AmbientCubemap
|
||||
cubemap && updateUsed(cubemap, textureResourceList);
|
||||
}
|
||||
}
|
||||
|
||||
export default class GPUResourceManager {
|
||||
private _renderer: Renderer;
|
||||
private _texturesList: Texture[] = [];
|
||||
private _geometriesList: Geometry[] = [];
|
||||
|
||||
constructor(renderer: Renderer) {
|
||||
this._renderer = renderer;
|
||||
}
|
||||
|
||||
collect(scene: Scene) {
|
||||
const renderer = this._renderer;
|
||||
const texturesList = this._texturesList;
|
||||
const geometriesList = this._geometriesList;
|
||||
// Mark all resources unused;
|
||||
markUnused(texturesList);
|
||||
markUnused(geometriesList);
|
||||
|
||||
// Collect resources used in this frame.
|
||||
const newTexturesList: Texture[] = [];
|
||||
const newGeometriesList: Geometry[] = [];
|
||||
collectResources(scene, newTexturesList, newGeometriesList);
|
||||
|
||||
// Dispose those unsed resources.
|
||||
checkAndDispose(renderer, texturesList);
|
||||
checkAndDispose(renderer, geometriesList);
|
||||
|
||||
this._texturesList = newTexturesList;
|
||||
this._geometriesList = newGeometriesList;
|
||||
}
|
||||
}
|
||||
1402
src/application.ts
1402
src/application.ts
File diff suppressed because it is too large
Load Diff
@ -68,3 +68,5 @@ export { default as Skybox } from './plugin/Skybox';
|
||||
|
||||
export * as meshUtil from './util/mesh';
|
||||
export * as textureUtil from './util/texture';
|
||||
|
||||
export { default as App3D } from './App3D';
|
||||
|
||||
@ -9,7 +9,7 @@ interface SurfaceGenerator {
|
||||
z: (u: number, v: number) => number;
|
||||
}
|
||||
|
||||
interface ParametricSurfaceGeometryOpts extends GeometryOpts {
|
||||
export interface ParametricSurfaceGeometryOpts extends GeometryOpts {
|
||||
generator: SurfaceGenerator;
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import Light, { LightOpts } from '../Light';
|
||||
|
||||
export interface AmbientSHLightOpts extends LightOpts {
|
||||
coefficients: number[];
|
||||
coefficients: ArrayLike<number>;
|
||||
}
|
||||
class AmbientSHLight extends Light {
|
||||
coefficients: number[];
|
||||
coefficients: ArrayLike<number>;
|
||||
|
||||
readonly type = 'AMBIENT_SH_LIGHT';
|
||||
|
||||
|
||||
@ -246,7 +246,7 @@ interface GLTFLoadOpts {
|
||||
|
||||
export function load(
|
||||
url: string,
|
||||
opts?: Omit<GLTFLoadOpts, 'onload' | 'onerror'>
|
||||
opts?: Partial<Omit<GLTFLoadOpts, 'onload' | 'onerror'>>
|
||||
): Promise<GLTFLoadResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
doLoadGLTF(
|
||||
@ -263,7 +263,7 @@ export function load(
|
||||
});
|
||||
}
|
||||
|
||||
function doLoadGLTF(url: string, opts?: GLTFLoadOpts) {
|
||||
function doLoadGLTF(url: string, opts?: Partial<GLTFLoadOpts>) {
|
||||
opts = Object.assign(
|
||||
{
|
||||
useStandardMaterial: false,
|
||||
@ -316,7 +316,7 @@ function doLoadGLTF(url: string, opts?: GLTFLoadOpts) {
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @return {clay.loader.GLTF.Result}
|
||||
*/
|
||||
export function parseBinary(buffer: ArrayBuffer, opts: GLTFLoadOpts) {
|
||||
export function parseBinary(buffer: ArrayBuffer, opts: Partial<GLTFLoadOpts>) {
|
||||
const header = new Uint32Array(buffer, 0, 4);
|
||||
const onerror = opts.onerror;
|
||||
if (header[0] !== 0x46546c67) {
|
||||
@ -370,7 +370,11 @@ export function parseBinary(buffer: ArrayBuffer, opts: GLTFLoadOpts) {
|
||||
* @param {ArrayBuffer[]} [buffer]
|
||||
* @return {clay.loader.GLTF.Result}
|
||||
*/
|
||||
export function parse(json: GLTFFormat, buffers: ArrayBuffer[] | undefined, opts: GLTFLoadOpts) {
|
||||
export function parse(
|
||||
json: GLTFFormat,
|
||||
buffers: ArrayBuffer[] | undefined,
|
||||
opts: Partial<GLTFLoadOpts>
|
||||
) {
|
||||
const lib: ParsedLib = {
|
||||
json: json,
|
||||
buffers: [],
|
||||
@ -494,7 +498,7 @@ export function parse(json: GLTFFormat, buffers: ArrayBuffer[] | undefined, opts
|
||||
* Binary file path resolver. User can override it
|
||||
* @param {string} path
|
||||
*/
|
||||
function resolveBufferPath(path: string, opts: GLTFLoadOpts) {
|
||||
function resolveBufferPath(path: string, opts: Partial<GLTFLoadOpts>) {
|
||||
if (path && path.match(/^data:(.*?)base64,/)) {
|
||||
return path;
|
||||
}
|
||||
@ -506,7 +510,7 @@ function resolveBufferPath(path: string, opts: GLTFLoadOpts) {
|
||||
* Texture file path resolver. User can override it
|
||||
* @param {string} path
|
||||
*/
|
||||
function resolveTexturePath(path: string, opts: GLTFLoadOpts) {
|
||||
function resolveTexturePath(path: string, opts: Partial<GLTFLoadOpts>) {
|
||||
if (path && path.match(/^data:(.*?)base64,/)) {
|
||||
return path;
|
||||
}
|
||||
@ -518,7 +522,7 @@ function loadBuffers(
|
||||
path: string,
|
||||
onsuccess: (buffer: ArrayBuffer) => void,
|
||||
onerror: (err: any) => void,
|
||||
opts: GLTFLoadOpts
|
||||
opts: Partial<GLTFLoadOpts>
|
||||
) {
|
||||
const base64Prefix = 'data:application/octet-stream;base64,';
|
||||
const strStart = path.substr(0, base64Prefix.length);
|
||||
@ -540,7 +544,7 @@ function loadBuffers(
|
||||
|
||||
// https://github.com/KhronosGroup/glTF/issues/100
|
||||
// https://github.com/KhronosGroup/glTF/issues/193
|
||||
function parseSkins(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseSkins(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
// Create skeletons and joints
|
||||
(json.skins || []).forEach((skinInfo: GLTFSkin, idx: number) => {
|
||||
const skeleton = new Skeleton(skinInfo.name);
|
||||
@ -600,7 +604,7 @@ function parseSkins(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseTextures(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseTextures(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
(json.textures || []).forEach((textureInfo: GLTFTexture, idx: number) => {
|
||||
// samplers is optional
|
||||
const samplerInfo = (json.samplers && json.samplers[textureInfo.sampler]) || {};
|
||||
@ -648,7 +652,7 @@ function parseTextures(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function KHRCommonMaterialToStandard(
|
||||
materialInfo: GLTFMaterial,
|
||||
lib: ParsedLib,
|
||||
opts: GLTFLoadOpts
|
||||
opts: Partial<GLTFLoadOpts>
|
||||
) {
|
||||
/* eslint-disable-next-line */
|
||||
const commonMaterialInfo = materialInfo.extensions['KHR_materials_common'];
|
||||
@ -760,7 +764,7 @@ function pbrMetallicRoughnessToStandard(
|
||||
materialInfo: GLTFMaterial,
|
||||
metallicRoughnessMatInfo: any,
|
||||
lib: ParsedLib,
|
||||
opts: GLTFLoadOpts
|
||||
opts: Partial<GLTFLoadOpts>
|
||||
) {
|
||||
const alphaTest = materialInfo.alphaMode === 'MASK';
|
||||
|
||||
@ -879,7 +883,7 @@ function pbrSpecularGlossinessToStandard(
|
||||
materialInfo: GLTFMaterial,
|
||||
specularGlossinessMatInfo: any,
|
||||
lib: ParsedLib,
|
||||
opts: GLTFLoadOpts
|
||||
opts: Partial<GLTFLoadOpts>
|
||||
) {
|
||||
const alphaTest = materialInfo.alphaMode === 'MASK';
|
||||
|
||||
@ -963,7 +967,7 @@ function pbrSpecularGlossinessToStandard(
|
||||
return material;
|
||||
}
|
||||
|
||||
function parseMaterials(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseMaterials(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
(json.materials || []).forEach((materialInfo: GLTFMaterial, idx: number) => {
|
||||
/* eslint-disable-next-line */
|
||||
if (materialInfo.extensions && materialInfo.extensions['KHR_materials_common']) {
|
||||
@ -991,7 +995,7 @@ function parseMaterials(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseMeshes(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseMeshes(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
(json.meshes || []).forEach((meshInfo: GLTFMesh, idx: number) => {
|
||||
lib.meshes[idx] = [];
|
||||
// Geometry
|
||||
@ -1162,7 +1166,7 @@ function instanceCamera(json: GLTFFormat, nodeInfo: GLTFNode) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseNodes(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseNodes(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
function instanceMesh(mesh: Mesh): Mesh {
|
||||
return new Mesh({
|
||||
name: mesh.name,
|
||||
@ -1232,7 +1236,7 @@ function parseNodes(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseAnimations(json: GLTFFormat, lib: ParsedLib, opts: GLTFLoadOpts) {
|
||||
function parseAnimations(json: GLTFFormat, lib: ParsedLib, opts: Partial<GLTFLoadOpts>) {
|
||||
function checkChannelPath(channelInfo: GLTFChannel) {
|
||||
if (channelInfo.path === 'weights') {
|
||||
console.warn('GLTFLoader not support morph targets yet.');
|
||||
|
||||
@ -1,262 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import Base from '../core/Base';
|
||||
import Ray from '../math/Ray';
|
||||
import Vector2 from '../math/Vector2';
|
||||
import Vector3 from '../math/Vector3';
|
||||
import Matrix4 from '../math/Matrix4';
|
||||
import Renderable from '../Renderable';
|
||||
import * as glenum from '../core/glenum';
|
||||
import vec3 from '../glmatrix/vec3';
|
||||
|
||||
/**
|
||||
* @constructor clay.picking.RayPicking
|
||||
* @extends clay.core.Base
|
||||
*/
|
||||
const RayPicking = Base.extend(
|
||||
/** @lends clay.picking.RayPicking# */ {
|
||||
/**
|
||||
* Target scene
|
||||
* @type {clay.Scene}
|
||||
*/
|
||||
scene: null,
|
||||
/**
|
||||
* Target camera
|
||||
* @type {clay.Camera}
|
||||
*/
|
||||
camera: null,
|
||||
/**
|
||||
* Target renderer
|
||||
* @type {clay.Renderer}
|
||||
*/
|
||||
renderer: null
|
||||
},
|
||||
function () {
|
||||
this._ray = new Ray();
|
||||
this._ndc = new Vector2();
|
||||
},
|
||||
/** @lends clay.picking.RayPicking.prototype */
|
||||
{
|
||||
/**
|
||||
* Pick the nearest intersection object in the scene
|
||||
* @param {number} x Mouse position x
|
||||
* @param {number} y Mouse position y
|
||||
* @param {boolean} [forcePickAll=false] ignore ignorePicking
|
||||
* @return {clay.picking.RayPicking~Intersection}
|
||||
*/
|
||||
pick: function (x, y, forcePickAll) {
|
||||
const out = this.pickAll(x, y, [], forcePickAll);
|
||||
return out[0] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick all intersection objects, wich will be sorted from near to far
|
||||
* @param {number} x Mouse position x
|
||||
* @param {number} y Mouse position y
|
||||
* @param {Array} [output]
|
||||
* @param {boolean} [forcePickAll=false] ignore ignorePicking
|
||||
* @return {Array.<clay.picking.RayPicking~Intersection>}
|
||||
*/
|
||||
pickAll: function (x, y, output, forcePickAll) {
|
||||
this.renderer.screenToNDC(x, y, this._ndc);
|
||||
this.camera.castRay(this._ndc, this._ray);
|
||||
|
||||
output = output || [];
|
||||
|
||||
this._intersectNode(this.scene, output, forcePickAll || false);
|
||||
|
||||
output.sort(this._intersectionCompareFunc);
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
_intersectNode: function (node, out, forcePickAll) {
|
||||
if (node instanceof Renderable && node.isRenderable()) {
|
||||
if (
|
||||
(!node.ignorePicking || forcePickAll) &&
|
||||
// Only triangle mesh support ray picking
|
||||
((node.mode === glenum.TRIANGLES && node.geometry.isUseIndices()) ||
|
||||
// Or if geometry has it's own pickByRay, pick, implementation
|
||||
node.geometry.pickByRay ||
|
||||
node.geometry.pick)
|
||||
) {
|
||||
this._intersectRenderable(node, out);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node._children.length; i++) {
|
||||
this._intersectNode(node._children[i], out, forcePickAll);
|
||||
}
|
||||
},
|
||||
|
||||
_intersectRenderable: (function () {
|
||||
const v1 = new Vector3();
|
||||
const v2 = new Vector3();
|
||||
const v3 = new Vector3();
|
||||
const ray = new Ray();
|
||||
const worldInverse = new Matrix4();
|
||||
|
||||
return function (renderable, out) {
|
||||
let isSkinnedMesh = renderable.isSkinnedMesh();
|
||||
ray.copy(this._ray);
|
||||
Matrix4.invert(worldInverse, renderable.worldTransform);
|
||||
|
||||
// Skinned mesh will ignore the world transform.
|
||||
if (!isSkinnedMesh) {
|
||||
ray.applyTransform(worldInverse);
|
||||
}
|
||||
|
||||
const geometry = renderable.geometry;
|
||||
|
||||
const bbox = isSkinnedMesh ? renderable.skeleton.boundingBox : geometry.boundingBox;
|
||||
|
||||
if (bbox && !ray.intersectBoundingBox(bbox)) {
|
||||
return;
|
||||
}
|
||||
// Use user defined picking algorithm
|
||||
if (geometry.pick) {
|
||||
geometry.pick(this._ndc.x, this._ndc.y, this.renderer, this.camera, renderable, out);
|
||||
return;
|
||||
}
|
||||
// Use user defined ray picking algorithm
|
||||
else if (geometry.pickByRay) {
|
||||
geometry.pickByRay(ray, renderable, out);
|
||||
return;
|
||||
}
|
||||
|
||||
const cullBack =
|
||||
(renderable.cullFace === glenum.BACK && renderable.frontFace === glenum.CCW) ||
|
||||
(renderable.cullFace === glenum.FRONT && renderable.frontFace === glenum.CW);
|
||||
|
||||
let point;
|
||||
const indices = geometry.indices;
|
||||
const positionAttr = geometry.attributes.position;
|
||||
const weightAttr = geometry.attributes.weight;
|
||||
const jointAttr = geometry.attributes.joint;
|
||||
let skinMatricesArray;
|
||||
const skinMatrices = [];
|
||||
// Check if valid.
|
||||
if (!positionAttr || !positionAttr.value || !indices) {
|
||||
return;
|
||||
}
|
||||
if (isSkinnedMesh) {
|
||||
skinMatricesArray = renderable.skeleton.getSubSkinMatrices(
|
||||
renderable.__uid__,
|
||||
renderable.joints
|
||||
);
|
||||
for (let i = 0; i < renderable.joints.length; i++) {
|
||||
skinMatrices[i] = skinMatrices[i] || [];
|
||||
for (let k = 0; k < 16; k++) {
|
||||
skinMatrices[i][k] = skinMatricesArray[i * 16 + k];
|
||||
}
|
||||
}
|
||||
const pos = [];
|
||||
const weight = [];
|
||||
const joint = [];
|
||||
const skinnedPos = [];
|
||||
const tmp = [];
|
||||
let skinnedPositionAttr = geometry.attributes.skinnedPosition;
|
||||
if (!skinnedPositionAttr || !skinnedPositionAttr.value) {
|
||||
geometry.createAttribute('skinnedPosition', 'f', 3);
|
||||
skinnedPositionAttr = geometry.attributes.skinnedPosition;
|
||||
skinnedPositionAttr.init(geometry.vertexCount);
|
||||
}
|
||||
for (let i = 0; i < geometry.vertexCount; i++) {
|
||||
positionAttr.get(i, pos);
|
||||
weightAttr.get(i, weight);
|
||||
jointAttr.get(i, joint);
|
||||
weight[3] = 1 - weight[0] - weight[1] - weight[2];
|
||||
vec3.set(skinnedPos, 0, 0, 0);
|
||||
for (let k = 0; k < 4; k++) {
|
||||
if (joint[k] >= 0 && weight[k] > 1e-4) {
|
||||
vec3.transformMat4(tmp, pos, skinMatrices[joint[k]]);
|
||||
vec3.scaleAndAdd(skinnedPos, skinnedPos, tmp, weight[k]);
|
||||
}
|
||||
}
|
||||
skinnedPositionAttr.set(i, skinnedPos);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < indices.length; i += 3) {
|
||||
const i1 = indices[i];
|
||||
const i2 = indices[i + 1];
|
||||
const i3 = indices[i + 2];
|
||||
const finalPosAttr = isSkinnedMesh ? geometry.attributes.skinnedPosition : positionAttr;
|
||||
finalPosAttr.get(i1, v1.array);
|
||||
finalPosAttr.get(i2, v2.array);
|
||||
finalPosAttr.get(i3, v3.array);
|
||||
|
||||
if (cullBack) {
|
||||
point = ray.intersectTriangle(v1, v2, v3, renderable.culling);
|
||||
} else {
|
||||
point = ray.intersectTriangle(v1, v3, v2, renderable.culling);
|
||||
}
|
||||
if (point) {
|
||||
const pointW = new Vector3();
|
||||
if (!isSkinnedMesh) {
|
||||
Vector3.transformMat4(pointW, point, renderable.worldTransform);
|
||||
} else {
|
||||
// TODO point maybe not right.
|
||||
Vector3.copy(pointW, point);
|
||||
}
|
||||
out.push(
|
||||
new RayPicking.Intersection(
|
||||
point,
|
||||
pointW,
|
||||
renderable,
|
||||
[i1, i2, i3],
|
||||
i / 3,
|
||||
Vector3.dist(pointW, this._ray.origin)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
})(),
|
||||
|
||||
_intersectionCompareFunc: function (a, b) {
|
||||
return a.distance - b.distance;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @constructor clay.picking.RayPicking~Intersection
|
||||
* @param {clay.Vector3} point
|
||||
* @param {clay.Vector3} pointWorld
|
||||
* @param {clay.Node} target
|
||||
* @param {Array.<number>} triangle
|
||||
* @param {number} triangleIndex
|
||||
* @param {number} distance
|
||||
*/
|
||||
RayPicking.Intersection = function (point, pointWorld, target, triangle, triangleIndex, distance) {
|
||||
/**
|
||||
* Intersection point in local transform coordinates
|
||||
* @type {clay.Vector3}
|
||||
*/
|
||||
this.point = point;
|
||||
/**
|
||||
* Intersection point in world transform coordinates
|
||||
* @type {clay.Vector3}
|
||||
*/
|
||||
this.pointWorld = pointWorld;
|
||||
/**
|
||||
* Intersection scene node
|
||||
* @type {clay.Node}
|
||||
*/
|
||||
this.target = target;
|
||||
/**
|
||||
* Intersection triangle, which is an array of vertex index
|
||||
* @type {Array.<number>}
|
||||
*/
|
||||
this.triangle = triangle;
|
||||
/**
|
||||
* Index of intersection triangle.
|
||||
*/
|
||||
this.triangleIndex = triangleIndex;
|
||||
/**
|
||||
* Distance from intersection point to ray origin
|
||||
* @type {number}
|
||||
*/
|
||||
this.distance = distance;
|
||||
};
|
||||
|
||||
export default RayPicking;
|
||||
273
src/picking/rayPicking.ts
Normal file
273
src/picking/rayPicking.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import Ray from '../math/Ray';
|
||||
import Vector2 from '../math/Vector2';
|
||||
import Vector3 from '../math/Vector3';
|
||||
import Matrix4 from '../math/Matrix4';
|
||||
import { mat4, vec3, vec4 } from '../glmatrix';
|
||||
import type Renderable from '../Renderable';
|
||||
import * as glenum from '../core/glenum';
|
||||
import type Renderer from '../Renderer';
|
||||
import type Scene from '../Scene';
|
||||
import type Camera from '../Camera';
|
||||
import type ClayNode from '../Node';
|
||||
import type { GeometryAttribute } from '../GeometryBase';
|
||||
|
||||
/**
|
||||
* Pick all intersection objects, wich will be sorted from near to far
|
||||
* @param x Mouse position x
|
||||
* @param y Mouse position y
|
||||
* @param output
|
||||
* @param forcePickAll ignore ignorePicking
|
||||
*/
|
||||
export function pickAll(
|
||||
renderer: Renderer,
|
||||
scene: Scene,
|
||||
camera: Camera,
|
||||
x: number,
|
||||
y: number,
|
||||
output?: Intersection[],
|
||||
forcePickAll?: boolean
|
||||
): Intersection[] {
|
||||
const ray = new Ray();
|
||||
const ndc = new Vector2();
|
||||
renderer.screenToNDC(x, y, ndc);
|
||||
camera.castRay(ndc, ray);
|
||||
|
||||
output = output || [];
|
||||
|
||||
intersectNode(renderer, camera, ray, ndc, scene, output, forcePickAll || false);
|
||||
|
||||
output.sort(intersectionCompareFunc);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the nearest intersection object in the scene
|
||||
* @param x Mouse position x
|
||||
* @param y Mouse position y
|
||||
* @param forcePickAll ignore ignorePicking
|
||||
*/
|
||||
export function pick(
|
||||
renderer: Renderer,
|
||||
scene: Scene,
|
||||
camera: Camera,
|
||||
x: number,
|
||||
y: number,
|
||||
forcePickAll?: boolean
|
||||
): Intersection | undefined {
|
||||
return pickAll(renderer, scene, camera, x, y, [], forcePickAll)[0];
|
||||
}
|
||||
|
||||
function intersectNode(
|
||||
renderer: Renderer,
|
||||
camera: Camera,
|
||||
ray: Ray,
|
||||
ndc: Vector2,
|
||||
|
||||
node: ClayNode,
|
||||
out: Intersection[],
|
||||
forcePickAll: boolean
|
||||
) {
|
||||
if (node.isRenderable && node.isRenderable()) {
|
||||
if (
|
||||
(!node.ignorePicking || forcePickAll) &&
|
||||
// Only triangle mesh support ray picking
|
||||
((node.mode === glenum.TRIANGLES && node.geometry.isUseIndices()) ||
|
||||
// Or if geometry has it's own pickByRay, pick, implementation
|
||||
node.geometry.pickByRay ||
|
||||
node.geometry.pick)
|
||||
) {
|
||||
intersectRenderable(renderer, camera, ray, ndc, node, out);
|
||||
}
|
||||
}
|
||||
const childrenRef = node.childrenRef();
|
||||
for (let i = 0; i < childrenRef.length; i++) {
|
||||
intersectNode(renderer, camera, ray, ndc, childrenRef[i], out, forcePickAll);
|
||||
}
|
||||
}
|
||||
|
||||
const v1 = new Vector3();
|
||||
const v2 = new Vector3();
|
||||
const v3 = new Vector3();
|
||||
const ray = new Ray();
|
||||
const worldInverse = new Matrix4();
|
||||
|
||||
function intersectRenderable(
|
||||
renderer: Renderer,
|
||||
camera: Camera,
|
||||
ray: Ray,
|
||||
ndc: Vector2,
|
||||
renderable: Renderable,
|
||||
out: Intersection[]
|
||||
) {
|
||||
const isSkinnedMesh = renderable.isSkinnedMesh();
|
||||
ray.copy(ray);
|
||||
Matrix4.invert(worldInverse, renderable.worldTransform);
|
||||
|
||||
// Skinned mesh will ignore the world transform.
|
||||
if (!isSkinnedMesh) {
|
||||
ray.applyTransform(worldInverse);
|
||||
}
|
||||
|
||||
const geometry = renderable.geometry;
|
||||
|
||||
const bbox = isSkinnedMesh ? renderable.skeleton.boundingBox : geometry.boundingBox;
|
||||
|
||||
if (bbox && !ray.intersectBoundingBox(bbox)) {
|
||||
return;
|
||||
}
|
||||
// Use user defined picking algorithm
|
||||
if (geometry.pick) {
|
||||
geometry.pick(ndc.x, ndc.y, renderer, camera, renderable, out);
|
||||
return;
|
||||
}
|
||||
// Use user defined ray picking algorithm
|
||||
else if (geometry.pickByRay) {
|
||||
geometry.pickByRay(ray, renderable, out);
|
||||
return;
|
||||
}
|
||||
|
||||
const cullBack =
|
||||
(renderable.cullFace === glenum.BACK && renderable.frontFace === glenum.CCW) ||
|
||||
(renderable.cullFace === glenum.FRONT && renderable.frontFace === glenum.CW);
|
||||
|
||||
let point;
|
||||
const indices = geometry.indices;
|
||||
const positionAttr = geometry.attributes.position;
|
||||
const weightAttr = geometry.attributes.weight;
|
||||
const jointAttr = geometry.attributes.joint;
|
||||
let skinMatricesArray;
|
||||
const skinMatrices: mat4.Mat4Array[] = [];
|
||||
// Check if valid.
|
||||
if (!positionAttr || !positionAttr.value || !indices) {
|
||||
return;
|
||||
}
|
||||
if (isSkinnedMesh) {
|
||||
skinMatricesArray = renderable.skeleton.getSubSkinMatrices(
|
||||
renderable.__uid__,
|
||||
renderable.joints
|
||||
);
|
||||
for (let i = 0; i < renderable.joints.length; i++) {
|
||||
skinMatrices[i] = skinMatrices[i] || [];
|
||||
for (let k = 0; k < 16; k++) {
|
||||
skinMatrices[i][k] = skinMatricesArray[i * 16 + k];
|
||||
}
|
||||
}
|
||||
const pos = vec3.create();
|
||||
const weight = vec4.create();
|
||||
const joint = vec4.create();
|
||||
const skinnedPos = vec3.create();
|
||||
const tmp = vec3.create();
|
||||
let skinnedPositionAttr = geometry.attributes.skinnedPosition as GeometryAttribute<3>;
|
||||
if (!skinnedPositionAttr || !skinnedPositionAttr.value) {
|
||||
geometry.createAttribute('skinnedPosition', 'float', 3);
|
||||
skinnedPositionAttr = geometry.attributes.skinnedPosition as GeometryAttribute<3>;
|
||||
skinnedPositionAttr.init(geometry.vertexCount);
|
||||
}
|
||||
for (let i = 0; i < geometry.vertexCount; i++) {
|
||||
positionAttr.get(i, pos);
|
||||
weightAttr.get(i, weight);
|
||||
jointAttr.get(i, joint);
|
||||
weight[3] = 1 - weight[0] - weight[1] - weight[2];
|
||||
vec3.set(skinnedPos, 0, 0, 0);
|
||||
for (let k = 0; k < 4; k++) {
|
||||
if (joint[k] >= 0 && weight[k] > 1e-4) {
|
||||
vec3.transformMat4(tmp, pos, skinMatrices[joint[k]]);
|
||||
vec3.scaleAndAdd(skinnedPos, skinnedPos, tmp, weight[k]);
|
||||
}
|
||||
}
|
||||
skinnedPositionAttr.set(i, skinnedPos);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < indices.length; i += 3) {
|
||||
const i1 = indices[i];
|
||||
const i2 = indices[i + 1];
|
||||
const i3 = indices[i + 2];
|
||||
const finalPosAttr = isSkinnedMesh ? geometry.attributes.skinnedPosition : positionAttr;
|
||||
finalPosAttr.get(i1, v1.array);
|
||||
finalPosAttr.get(i2, v2.array);
|
||||
finalPosAttr.get(i3, v3.array);
|
||||
|
||||
if (cullBack) {
|
||||
point = ray.intersectTriangle(v1, v2, v3, renderable.culling);
|
||||
} else {
|
||||
point = ray.intersectTriangle(v1, v3, v2, renderable.culling);
|
||||
}
|
||||
if (point) {
|
||||
const pointW = new Vector3();
|
||||
if (!isSkinnedMesh) {
|
||||
Vector3.transformMat4(pointW, point, renderable.worldTransform);
|
||||
} else {
|
||||
// TODO point maybe not right.
|
||||
Vector3.copy(pointW, point);
|
||||
}
|
||||
out.push(
|
||||
new Intersection(
|
||||
point,
|
||||
pointW,
|
||||
renderable,
|
||||
[i1, i2, i3],
|
||||
i / 3,
|
||||
Vector3.dist(pointW, ray.origin)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function intersectionCompareFunc(a: Intersection, b: Intersection) {
|
||||
return a.distance - b.distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor clay.picking.RayPicking~Intersection
|
||||
* @param {clay.Vector3} point
|
||||
* @param {clay.Vector3} pointWorld
|
||||
* @param {clay.Node} target
|
||||
* @param {Array.<number>} triangle
|
||||
* @param {number} triangleIndex
|
||||
* @param {number} distance
|
||||
*/
|
||||
export class Intersection {
|
||||
/**
|
||||
* Intersection point in local transform coordinates
|
||||
*/
|
||||
point: Vector3;
|
||||
/**
|
||||
* Intersection point in world transform coordinates
|
||||
*/
|
||||
pointWorld: Vector3;
|
||||
/**
|
||||
* Intersection scene node
|
||||
*/
|
||||
target: ClayNode;
|
||||
/**
|
||||
* Intersection triangle, which is an array of vertex index
|
||||
*/
|
||||
triangle: number[];
|
||||
/**
|
||||
* Index of intersection triangle.
|
||||
*/
|
||||
triangleIndex: number;
|
||||
/**
|
||||
* Distance from intersection point to ray origin
|
||||
*/
|
||||
distance: number;
|
||||
|
||||
constructor(
|
||||
point: Vector3,
|
||||
pointWorld: Vector3,
|
||||
target: ClayNode,
|
||||
triangle: number[],
|
||||
triangleIndex: number,
|
||||
distance: number
|
||||
) {
|
||||
this.point = point;
|
||||
this.pointWorld = pointWorld;
|
||||
this.target = target;
|
||||
this.triangle = triangle;
|
||||
this.triangleIndex = triangleIndex;
|
||||
this.distance = distance;
|
||||
}
|
||||
}
|
||||
@ -141,6 +141,10 @@ class Skybox extends Mesh {
|
||||
}
|
||||
renderer.renderPass([this], dummyCamera);
|
||||
}
|
||||
|
||||
static getSceneSkybox(scene: Scene) {
|
||||
return sceneSkyboxMap.get(scene);
|
||||
}
|
||||
}
|
||||
|
||||
export default Skybox;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user