refact: provide App3D instead of option based application.create

This commit is contained in:
pissang 2022-05-03 22:11:38 +08:00
parent 391481f713
commit 30cbd2f7b2
16 changed files with 1918 additions and 1690 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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[];

View File

@ -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');

View File

@ -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 {

View File

@ -195,7 +195,7 @@ interface Viewport {
height: number;
devicePixelRatio: number;
}
interface RendererOpts {
export interface RendererOpts {
canvas: HTMLCanvasElement | null;
/**

View File

@ -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
View 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);
});
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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';

View File

@ -9,7 +9,7 @@ interface SurfaceGenerator {
z: (u: number, v: number) => number;
}
interface ParametricSurfaceGeometryOpts extends GeometryOpts {
export interface ParametricSurfaceGeometryOpts extends GeometryOpts {
generator: SurfaceGenerator;
}

View File

@ -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';

View File

@ -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.');

View File

@ -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
View 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;
}
}

View File

@ -141,6 +141,10 @@ class Skybox extends Mesh {
}
renderer.renderPass([this], dummyCamera);
}
static getSceneSkybox(scene: Scene) {
return sceneSkyboxMap.get(scene);
}
}
export default Skybox;