feat: initialize project with core dependencies and game entry point
This commit is contained in:
539
node_modules/three/examples/jsm/misc/ConvexObjectBreaker.js
generated
vendored
Normal file
539
node_modules/three/examples/jsm/misc/ConvexObjectBreaker.js
generated
vendored
Normal file
@@ -0,0 +1,539 @@
|
||||
import {
|
||||
Line3,
|
||||
Mesh,
|
||||
Plane,
|
||||
Vector3
|
||||
} from 'three';
|
||||
import { ConvexGeometry } from '../geometries/ConvexGeometry.js';
|
||||
|
||||
const _v1 = new Vector3();
|
||||
|
||||
/**
|
||||
* This class can be used to subdivide a convex Geometry object into pieces.
|
||||
*
|
||||
* Use the function prepareBreakableObject to prepare a Mesh object to be broken.
|
||||
* Then, call the various functions to subdivide the object (subdivideByImpact, cutByPlane).
|
||||
* Sub-objects that are product of subdivision don't need prepareBreakableObject to be called on them.
|
||||
*
|
||||
* Requisites for the object:
|
||||
* - Mesh object must have a buffer geometry and a material.
|
||||
* - Vertex normals must be planar (not smoothed).
|
||||
* - The geometry must be convex (this is not checked in the library). You can create convex
|
||||
* geometries with {@link ConvexGeometry}. The {@link BoxGeometry}, {@link SphereGeometry} and other
|
||||
* convex primitives can also be used.
|
||||
*
|
||||
* Note: This lib adds member variables to object's userData member (see prepareBreakableObject function)
|
||||
* Use with caution and read the code when using with other libs.
|
||||
*
|
||||
* @three_import import { ConvexObjectBreaker } from 'three/addons/misc/ConvexObjectBreaker.js';
|
||||
*/
|
||||
class ConvexObjectBreaker {
|
||||
|
||||
/**
|
||||
* Constructs a new convex object breaker.
|
||||
*
|
||||
* @param {number} [minSizeForBreak=1.4] - Min size a debris can have to break.
|
||||
* @param {number} [smallDelta=0.0001] - Max distance to consider that a point belongs to a plane.
|
||||
*/
|
||||
constructor( minSizeForBreak = 1.4, smallDelta = 0.0001 ) {
|
||||
|
||||
this.minSizeForBreak = minSizeForBreak;
|
||||
this.smallDelta = smallDelta;
|
||||
|
||||
this.tempLine1 = new Line3();
|
||||
this.tempPlane1 = new Plane();
|
||||
this.tempPlane2 = new Plane();
|
||||
this.tempPlane_Cut = new Plane();
|
||||
this.tempCM1 = new Vector3();
|
||||
this.tempCM2 = new Vector3();
|
||||
this.tempVector3 = new Vector3();
|
||||
this.tempVector3_2 = new Vector3();
|
||||
this.tempVector3_3 = new Vector3();
|
||||
this.tempVector3_P0 = new Vector3();
|
||||
this.tempVector3_P1 = new Vector3();
|
||||
this.tempVector3_P2 = new Vector3();
|
||||
this.tempVector3_N0 = new Vector3();
|
||||
this.tempVector3_N1 = new Vector3();
|
||||
this.tempVector3_AB = new Vector3();
|
||||
this.tempVector3_CB = new Vector3();
|
||||
this.tempResultObjects = { object1: null, object2: null };
|
||||
|
||||
this.segments = [];
|
||||
const n = 30 * 30;
|
||||
for ( let i = 0; i < n; i ++ ) this.segments[ i ] = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called for all 3D objects that should be breakable.
|
||||
*
|
||||
* @param {Object3D} object - The 3D object. It must have a convex geometry.
|
||||
* @param {number} mass - The 3D object's mass in kg. Must be greater than `0`.
|
||||
* @param {Vector3} velocity - The 3D object's velocity.
|
||||
* @param {Vector3} angularVelocity - The 3D object's angular velocity.
|
||||
* @param {boolean} breakable - Whether the 3D object is breakable or not.
|
||||
*/
|
||||
prepareBreakableObject( object, mass, velocity, angularVelocity, breakable ) {
|
||||
|
||||
// object is a Object3d (normally a Mesh), must have a buffer geometry, and it must be convex.
|
||||
// Its material property is propagated to its children (sub-pieces)
|
||||
// mass must be > 0
|
||||
|
||||
const userData = object.userData;
|
||||
userData.mass = mass;
|
||||
userData.velocity = velocity.clone();
|
||||
userData.angularVelocity = angularVelocity.clone();
|
||||
userData.breakable = breakable;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Subdivides the given 3D object into pieces by an impact (meaning another object hits
|
||||
* the given 3D object at a certain surface point).
|
||||
*
|
||||
* @param {Object3D} object - The 3D object to subdivide.
|
||||
* @param {Vector3} pointOfImpact - The point of impact.
|
||||
* @param {Vector3} normal - The impact normal.
|
||||
* @param {number} maxRadialIterations - Iterations for radial cuts.
|
||||
* @param {number} maxRandomIterations - Max random iterations for not-radial cuts.
|
||||
* @return {Array<Object3D>} The array of pieces.
|
||||
*/
|
||||
subdivideByImpact( object, pointOfImpact, normal, maxRadialIterations, maxRandomIterations ) {
|
||||
|
||||
const debris = [];
|
||||
|
||||
const tempPlane1 = this.tempPlane1;
|
||||
const tempPlane2 = this.tempPlane2;
|
||||
|
||||
this.tempVector3.addVectors( pointOfImpact, normal );
|
||||
tempPlane1.setFromCoplanarPoints( pointOfImpact, object.position, this.tempVector3 );
|
||||
|
||||
const maxTotalIterations = maxRandomIterations + maxRadialIterations;
|
||||
|
||||
const scope = this;
|
||||
|
||||
function subdivideRadial( subObject, startAngle, endAngle, numIterations ) {
|
||||
|
||||
if ( Math.random() < numIterations * 0.05 || numIterations > maxTotalIterations ) {
|
||||
|
||||
debris.push( subObject );
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
let angle = Math.PI;
|
||||
|
||||
if ( numIterations === 0 ) {
|
||||
|
||||
tempPlane2.normal.copy( tempPlane1.normal );
|
||||
tempPlane2.constant = tempPlane1.constant;
|
||||
|
||||
} else {
|
||||
|
||||
if ( numIterations <= maxRadialIterations ) {
|
||||
|
||||
angle = ( endAngle - startAngle ) * ( 0.2 + 0.6 * Math.random() ) + startAngle;
|
||||
|
||||
// Rotate tempPlane2 at impact point around normal axis and the angle
|
||||
scope.tempVector3_2.copy( object.position ).sub( pointOfImpact ).applyAxisAngle( normal, angle ).add( pointOfImpact );
|
||||
tempPlane2.setFromCoplanarPoints( pointOfImpact, scope.tempVector3, scope.tempVector3_2 );
|
||||
|
||||
} else {
|
||||
|
||||
angle = ( ( 0.5 * ( numIterations & 1 ) ) + 0.2 * ( 2 - Math.random() ) ) * Math.PI;
|
||||
|
||||
// Rotate tempPlane2 at object position around normal axis and the angle
|
||||
scope.tempVector3_2.copy( pointOfImpact ).sub( subObject.position ).applyAxisAngle( normal, angle ).add( subObject.position );
|
||||
scope.tempVector3_3.copy( normal ).add( subObject.position );
|
||||
tempPlane2.setFromCoplanarPoints( subObject.position, scope.tempVector3_3, scope.tempVector3_2 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Perform the cut
|
||||
scope.cutByPlane( subObject, tempPlane2, scope.tempResultObjects );
|
||||
|
||||
const obj1 = scope.tempResultObjects.object1;
|
||||
const obj2 = scope.tempResultObjects.object2;
|
||||
|
||||
if ( obj1 ) {
|
||||
|
||||
subdivideRadial( obj1, startAngle, angle, numIterations + 1 );
|
||||
|
||||
}
|
||||
|
||||
if ( obj2 ) {
|
||||
|
||||
subdivideRadial( obj2, angle, endAngle, numIterations + 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
subdivideRadial( object, 0, 2 * Math.PI, 0 );
|
||||
|
||||
return debris;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Subdivides the given 3D object into pieces by a plane.
|
||||
*
|
||||
* @param {Object3D} object - The 3D object to subdivide.
|
||||
* @param {Plane} plane - The plane to cut the 3D object.
|
||||
* @param {{object1:?Mesh,object2:?Mesh}} output - An object that stores the pieces.
|
||||
* @return {number} The number of pieces.
|
||||
*/
|
||||
cutByPlane( object, plane, output ) {
|
||||
|
||||
// Returns breakable objects in output.object1 and output.object2 members, the resulting 2 pieces of the cut.
|
||||
// object2 can be null if the plane doesn't cut the object.
|
||||
// object1 can be null only in case of internal error
|
||||
// Returned value is number of pieces, 0 for error.
|
||||
|
||||
const geometry = object.geometry;
|
||||
const coords = geometry.attributes.position.array;
|
||||
const normals = geometry.attributes.normal.array;
|
||||
|
||||
const numPoints = coords.length / 3;
|
||||
let numFaces = numPoints / 3;
|
||||
|
||||
let indices = geometry.getIndex();
|
||||
|
||||
if ( indices ) {
|
||||
|
||||
indices = indices.array;
|
||||
numFaces = indices.length / 3;
|
||||
|
||||
}
|
||||
|
||||
function getVertexIndex( faceIdx, vert ) {
|
||||
|
||||
// vert = 0, 1 or 2.
|
||||
|
||||
const idx = faceIdx * 3 + vert;
|
||||
|
||||
return indices ? indices[ idx ] : idx;
|
||||
|
||||
}
|
||||
|
||||
const points1 = [];
|
||||
const points2 = [];
|
||||
|
||||
const delta = this.smallDelta;
|
||||
|
||||
// Reset segments mark
|
||||
const numPointPairs = numPoints * numPoints;
|
||||
for ( let i = 0; i < numPointPairs; i ++ ) this.segments[ i ] = false;
|
||||
|
||||
const p0 = this.tempVector3_P0;
|
||||
const p1 = this.tempVector3_P1;
|
||||
const n0 = this.tempVector3_N0;
|
||||
const n1 = this.tempVector3_N1;
|
||||
|
||||
// Iterate through the faces to mark edges shared by coplanar faces
|
||||
for ( let i = 0; i < numFaces - 1; i ++ ) {
|
||||
|
||||
const a1 = getVertexIndex( i, 0 );
|
||||
const b1 = getVertexIndex( i, 1 );
|
||||
const c1 = getVertexIndex( i, 2 );
|
||||
|
||||
// Assuming all 3 vertices have the same normal
|
||||
n0.set( normals[ a1 ], normals[ a1 ] + 1, normals[ a1 ] + 2 );
|
||||
|
||||
for ( let j = i + 1; j < numFaces; j ++ ) {
|
||||
|
||||
const a2 = getVertexIndex( j, 0 );
|
||||
const b2 = getVertexIndex( j, 1 );
|
||||
const c2 = getVertexIndex( j, 2 );
|
||||
|
||||
// Assuming all 3 vertices have the same normal
|
||||
n1.set( normals[ a2 ], normals[ a2 ] + 1, normals[ a2 ] + 2 );
|
||||
|
||||
const coplanar = 1 - n0.dot( n1 ) < delta;
|
||||
|
||||
if ( coplanar ) {
|
||||
|
||||
if ( a1 === a2 || a1 === b2 || a1 === c2 ) {
|
||||
|
||||
if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
|
||||
|
||||
this.segments[ a1 * numPoints + b1 ] = true;
|
||||
this.segments[ b1 * numPoints + a1 ] = true;
|
||||
|
||||
} else {
|
||||
|
||||
this.segments[ c1 * numPoints + a1 ] = true;
|
||||
this.segments[ a1 * numPoints + c1 ] = true;
|
||||
|
||||
}
|
||||
|
||||
} else if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
|
||||
|
||||
this.segments[ c1 * numPoints + b1 ] = true;
|
||||
this.segments[ b1 * numPoints + c1 ] = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Transform the plane to object local space
|
||||
const localPlane = this.tempPlane_Cut;
|
||||
object.updateMatrix();
|
||||
ConvexObjectBreaker.transformPlaneToLocalSpace( plane, object.matrix, localPlane );
|
||||
|
||||
// Iterate through the faces adding points to both pieces
|
||||
for ( let i = 0; i < numFaces; i ++ ) {
|
||||
|
||||
const va = getVertexIndex( i, 0 );
|
||||
const vb = getVertexIndex( i, 1 );
|
||||
const vc = getVertexIndex( i, 2 );
|
||||
|
||||
for ( let segment = 0; segment < 3; segment ++ ) {
|
||||
|
||||
const i0 = segment === 0 ? va : ( segment === 1 ? vb : vc );
|
||||
const i1 = segment === 0 ? vb : ( segment === 1 ? vc : va );
|
||||
|
||||
const segmentState = this.segments[ i0 * numPoints + i1 ];
|
||||
|
||||
if ( segmentState ) continue; // The segment already has been processed in another face
|
||||
|
||||
// Mark segment as processed (also inverted segment)
|
||||
this.segments[ i0 * numPoints + i1 ] = true;
|
||||
this.segments[ i1 * numPoints + i0 ] = true;
|
||||
|
||||
p0.set( coords[ 3 * i0 ], coords[ 3 * i0 + 1 ], coords[ 3 * i0 + 2 ] );
|
||||
p1.set( coords[ 3 * i1 ], coords[ 3 * i1 + 1 ], coords[ 3 * i1 + 2 ] );
|
||||
|
||||
// mark: 1 for negative side, 2 for positive side, 3 for coplanar point
|
||||
let mark0 = 0;
|
||||
|
||||
let d = localPlane.distanceToPoint( p0 );
|
||||
|
||||
if ( d > delta ) {
|
||||
|
||||
mark0 = 2;
|
||||
points2.push( p0.clone() );
|
||||
|
||||
} else if ( d < - delta ) {
|
||||
|
||||
mark0 = 1;
|
||||
points1.push( p0.clone() );
|
||||
|
||||
} else {
|
||||
|
||||
mark0 = 3;
|
||||
points1.push( p0.clone() );
|
||||
points2.push( p0.clone() );
|
||||
|
||||
}
|
||||
|
||||
// mark: 1 for negative side, 2 for positive side, 3 for coplanar point
|
||||
let mark1 = 0;
|
||||
|
||||
d = localPlane.distanceToPoint( p1 );
|
||||
|
||||
if ( d > delta ) {
|
||||
|
||||
mark1 = 2;
|
||||
points2.push( p1.clone() );
|
||||
|
||||
} else if ( d < - delta ) {
|
||||
|
||||
mark1 = 1;
|
||||
points1.push( p1.clone() );
|
||||
|
||||
} else {
|
||||
|
||||
mark1 = 3;
|
||||
points1.push( p1.clone() );
|
||||
points2.push( p1.clone() );
|
||||
|
||||
}
|
||||
|
||||
if ( ( mark0 === 1 && mark1 === 2 ) || ( mark0 === 2 && mark1 === 1 ) ) {
|
||||
|
||||
// Intersection of segment with the plane
|
||||
|
||||
this.tempLine1.start.copy( p0 );
|
||||
this.tempLine1.end.copy( p1 );
|
||||
|
||||
let intersection = new Vector3();
|
||||
intersection = localPlane.intersectLine( this.tempLine1, intersection );
|
||||
|
||||
if ( intersection === null ) {
|
||||
|
||||
// Shouldn't happen
|
||||
console.error( 'Internal error: segment does not intersect plane.' );
|
||||
output.segmentedObject1 = null;
|
||||
output.segmentedObject2 = null;
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
points1.push( intersection );
|
||||
points2.push( intersection.clone() );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Calculate debris mass (very fast and imprecise):
|
||||
const newMass = object.userData.mass * 0.5;
|
||||
|
||||
// Calculate debris Center of Mass (again fast and imprecise)
|
||||
this.tempCM1.set( 0, 0, 0 );
|
||||
let radius1 = 0;
|
||||
const numPoints1 = points1.length;
|
||||
|
||||
if ( numPoints1 > 0 ) {
|
||||
|
||||
for ( let i = 0; i < numPoints1; i ++ ) this.tempCM1.add( points1[ i ] );
|
||||
|
||||
this.tempCM1.divideScalar( numPoints1 );
|
||||
for ( let i = 0; i < numPoints1; i ++ ) {
|
||||
|
||||
const p = points1[ i ];
|
||||
p.sub( this.tempCM1 );
|
||||
radius1 = Math.max( radius1, p.x, p.y, p.z );
|
||||
|
||||
}
|
||||
|
||||
this.tempCM1.add( object.position );
|
||||
|
||||
}
|
||||
|
||||
this.tempCM2.set( 0, 0, 0 );
|
||||
let radius2 = 0;
|
||||
const numPoints2 = points2.length;
|
||||
if ( numPoints2 > 0 ) {
|
||||
|
||||
for ( let i = 0; i < numPoints2; i ++ ) this.tempCM2.add( points2[ i ] );
|
||||
|
||||
this.tempCM2.divideScalar( numPoints2 );
|
||||
for ( let i = 0; i < numPoints2; i ++ ) {
|
||||
|
||||
const p = points2[ i ];
|
||||
p.sub( this.tempCM2 );
|
||||
radius2 = Math.max( radius2, p.x, p.y, p.z );
|
||||
|
||||
}
|
||||
|
||||
this.tempCM2.add( object.position );
|
||||
|
||||
}
|
||||
|
||||
let object1 = null;
|
||||
let object2 = null;
|
||||
|
||||
let numObjects = 0;
|
||||
|
||||
if ( numPoints1 > 4 ) {
|
||||
|
||||
object1 = new Mesh( new ConvexGeometry( points1 ), object.material );
|
||||
object1.position.copy( this.tempCM1 );
|
||||
object1.quaternion.copy( object.quaternion );
|
||||
|
||||
this.prepareBreakableObject( object1, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius1 > this.minSizeForBreak );
|
||||
|
||||
numObjects ++;
|
||||
|
||||
}
|
||||
|
||||
if ( numPoints2 > 4 ) {
|
||||
|
||||
object2 = new Mesh( new ConvexGeometry( points2 ), object.material );
|
||||
object2.position.copy( this.tempCM2 );
|
||||
object2.quaternion.copy( object.quaternion );
|
||||
|
||||
this.prepareBreakableObject( object2, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius2 > this.minSizeForBreak );
|
||||
|
||||
numObjects ++;
|
||||
|
||||
}
|
||||
|
||||
output.object1 = object1;
|
||||
output.object2 = object2;
|
||||
|
||||
return numObjects;
|
||||
|
||||
}
|
||||
|
||||
// internal helpers
|
||||
|
||||
static transformFreeVector( v, m ) {
|
||||
|
||||
// input:
|
||||
// vector interpreted as a free vector
|
||||
// THREE.Matrix4 orthogonal matrix (matrix without scale)
|
||||
|
||||
const x = v.x, y = v.y, z = v.z;
|
||||
const e = m.elements;
|
||||
|
||||
v.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
|
||||
v.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
|
||||
v.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;
|
||||
|
||||
return v;
|
||||
|
||||
}
|
||||
|
||||
static transformFreeVectorInverse( v, m ) {
|
||||
|
||||
// input:
|
||||
// vector interpreted as a free vector
|
||||
// THREE.Matrix4 orthogonal matrix (matrix without scale)
|
||||
|
||||
const x = v.x, y = v.y, z = v.z;
|
||||
const e = m.elements;
|
||||
|
||||
v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z;
|
||||
v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z;
|
||||
v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z;
|
||||
|
||||
return v;
|
||||
|
||||
}
|
||||
|
||||
static transformTiedVectorInverse( v, m ) {
|
||||
|
||||
// input:
|
||||
// vector interpreted as a tied (ordinary) vector
|
||||
// THREE.Matrix4 orthogonal matrix (matrix without scale)
|
||||
|
||||
const x = v.x, y = v.y, z = v.z;
|
||||
const e = m.elements;
|
||||
|
||||
v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z - e[ 12 ];
|
||||
v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z - e[ 13 ];
|
||||
v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z - e[ 14 ];
|
||||
|
||||
return v;
|
||||
|
||||
}
|
||||
|
||||
static transformPlaneToLocalSpace( plane, m, resultPlane ) {
|
||||
|
||||
resultPlane.normal.copy( plane.normal );
|
||||
resultPlane.constant = plane.constant;
|
||||
|
||||
const referencePoint = ConvexObjectBreaker.transformTiedVectorInverse( plane.coplanarPoint( _v1 ), m );
|
||||
|
||||
ConvexObjectBreaker.transformFreeVectorInverse( resultPlane.normal, m );
|
||||
|
||||
// recalculate constant (like in setFromNormalAndCoplanarPoint)
|
||||
resultPlane.constant = - referencePoint.dot( resultPlane.normal );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ConvexObjectBreaker };
|
||||
506
node_modules/three/examples/jsm/misc/GPUComputationRenderer.js
generated
vendored
Normal file
506
node_modules/three/examples/jsm/misc/GPUComputationRenderer.js
generated
vendored
Normal file
@@ -0,0 +1,506 @@
|
||||
import {
|
||||
ClampToEdgeWrapping,
|
||||
DataTexture,
|
||||
FloatType,
|
||||
NearestFilter,
|
||||
RGBAFormat,
|
||||
ShaderMaterial,
|
||||
WebGLRenderTarget
|
||||
} from 'three';
|
||||
|
||||
import { FullScreenQuad } from '../postprocessing/Pass.js';
|
||||
|
||||
/**
|
||||
* GPUComputationRenderer, based on SimulationRenderer by @zz85.
|
||||
*
|
||||
* The GPUComputationRenderer uses the concept of variables. These variables are RGBA float textures that hold 4 floats
|
||||
* for each compute element (texel).
|
||||
*
|
||||
* Each variable has a fragment shader that defines the computation made to obtain the variable in question.
|
||||
* You can use as many variables you need, and make dependencies so you can use textures of other variables in the shader
|
||||
* (the sampler uniforms are added automatically) Most of the variables will need themselves as dependency.
|
||||
*
|
||||
* The renderer has actually two render targets per variable, to make ping-pong. Textures from the current frame are used
|
||||
* as inputs to render the textures of the next frame.
|
||||
*
|
||||
* The render targets of the variables can be used as input textures for your visualization shaders.
|
||||
*
|
||||
* Variable names should be valid identifiers and should not collide with THREE GLSL used identifiers.
|
||||
* a common approach could be to use 'texture' prefixing the variable name; i.e texturePosition, textureVelocity...
|
||||
*
|
||||
* The size of the computation (sizeX * sizeY) is defined as 'resolution' automatically in the shader. For example:
|
||||
* ```
|
||||
* #DEFINE resolution vec2( 1024.0, 1024.0 )
|
||||
* ```
|
||||
* Basic use:
|
||||
* ```js
|
||||
* // Initialization...
|
||||
*
|
||||
* // Create computation renderer
|
||||
* const gpuCompute = new GPUComputationRenderer( 1024, 1024, renderer );
|
||||
*
|
||||
* // Create initial state float textures
|
||||
* const pos0 = gpuCompute.createTexture();
|
||||
* const vel0 = gpuCompute.createTexture();
|
||||
* // and fill in here the texture data...
|
||||
*
|
||||
* // Add texture variables
|
||||
* const velVar = gpuCompute.addVariable( "textureVelocity", fragmentShaderVel, vel0 );
|
||||
* const posVar = gpuCompute.addVariable( "texturePosition", fragmentShaderPos, pos0 );
|
||||
*
|
||||
* // Add variable dependencies
|
||||
* gpuCompute.setVariableDependencies( velVar, [ velVar, posVar ] );
|
||||
* gpuCompute.setVariableDependencies( posVar, [ velVar, posVar ] );
|
||||
*
|
||||
* // Add custom uniforms
|
||||
* velVar.material.uniforms.time = { value: 0.0 };
|
||||
*
|
||||
* // Check for completeness
|
||||
* const error = gpuCompute.init();
|
||||
* if ( error !== null ) {
|
||||
* console.error( error );
|
||||
* }
|
||||
*
|
||||
* // In each frame...
|
||||
*
|
||||
* // Compute!
|
||||
* gpuCompute.compute();
|
||||
*
|
||||
* // Update texture uniforms in your visualization materials with the gpu renderer output
|
||||
* myMaterial.uniforms.myTexture.value = gpuCompute.getCurrentRenderTarget( posVar ).texture;
|
||||
*
|
||||
* // Do your rendering
|
||||
* renderer.render( myScene, myCamera );
|
||||
* ```
|
||||
*
|
||||
* Also, you can use utility functions to create ShaderMaterial and perform computations (rendering between textures)
|
||||
* Note that the shaders can have multiple input textures.
|
||||
*
|
||||
* ```js
|
||||
* const myFilter1 = gpuCompute.createShaderMaterial( myFilterFragmentShader1, { theTexture: { value: null } } );
|
||||
* const myFilter2 = gpuCompute.createShaderMaterial( myFilterFragmentShader2, { theTexture: { value: null } } );
|
||||
*
|
||||
* const inputTexture = gpuCompute.createTexture();
|
||||
*
|
||||
* // Fill in here inputTexture...
|
||||
*
|
||||
* myFilter1.uniforms.theTexture.value = inputTexture;
|
||||
*
|
||||
* const myRenderTarget = gpuCompute.createRenderTarget();
|
||||
* myFilter2.uniforms.theTexture.value = myRenderTarget.texture;
|
||||
*
|
||||
* const outputRenderTarget = gpuCompute.createRenderTarget();
|
||||
*
|
||||
* // Now use the output texture where you want:
|
||||
* myMaterial.uniforms.map.value = outputRenderTarget.texture;
|
||||
*
|
||||
* // And compute each frame, before rendering to screen:
|
||||
* gpuCompute.doRenderTarget( myFilter1, myRenderTarget );
|
||||
* gpuCompute.doRenderTarget( myFilter2, outputRenderTarget );
|
||||
* ```
|
||||
*
|
||||
* @three_import import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js';
|
||||
*/
|
||||
class GPUComputationRenderer {
|
||||
|
||||
/**
|
||||
* Constructs a new GPU computation renderer.
|
||||
*
|
||||
* @param {number} sizeX - Computation problem size is always 2d: sizeX * sizeY elements.
|
||||
* @param {number} sizeY - Computation problem size is always 2d: sizeX * sizeY elements.
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
*/
|
||||
constructor( sizeX, sizeY, renderer ) {
|
||||
|
||||
this.variables = [];
|
||||
|
||||
this.currentTextureIndex = 0;
|
||||
|
||||
let dataType = FloatType;
|
||||
|
||||
const passThruUniforms = {
|
||||
passThruTexture: { value: null }
|
||||
};
|
||||
|
||||
const passThruShader = createShaderMaterial( getPassThroughFragmentShader(), passThruUniforms );
|
||||
|
||||
const quad = new FullScreenQuad( passThruShader );
|
||||
|
||||
/**
|
||||
* Sets the data type of the internal textures.
|
||||
*
|
||||
* @param {(FloatType|HalfFloatType)} type - The type to set.
|
||||
* @return {GPUComputationRenderer} A reference to this renderer.
|
||||
*/
|
||||
this.setDataType = function ( type ) {
|
||||
|
||||
dataType = type;
|
||||
return this;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a compute variable to the renderer.
|
||||
*
|
||||
* @param {string} variableName - The variable name.
|
||||
* @param {string} computeFragmentShader - The compute (fragment) shader source.
|
||||
* @param {Texture} initialValueTexture - The initial value texture.
|
||||
* @return {Object} The compute variable.
|
||||
*/
|
||||
this.addVariable = function ( variableName, computeFragmentShader, initialValueTexture ) {
|
||||
|
||||
const material = this.createShaderMaterial( computeFragmentShader );
|
||||
|
||||
const variable = {
|
||||
name: variableName,
|
||||
initialValueTexture: initialValueTexture,
|
||||
material: material,
|
||||
dependencies: null,
|
||||
renderTargets: [],
|
||||
wrapS: null,
|
||||
wrapT: null,
|
||||
minFilter: NearestFilter,
|
||||
magFilter: NearestFilter
|
||||
};
|
||||
|
||||
this.variables.push( variable );
|
||||
|
||||
return variable;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets variable dependencies.
|
||||
*
|
||||
* @param {Object} variable - The compute variable.
|
||||
* @param {Array<Object>} dependencies - Other compute variables that represents the dependencies.
|
||||
*/
|
||||
this.setVariableDependencies = function ( variable, dependencies ) {
|
||||
|
||||
variable.dependencies = dependencies;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the renderer.
|
||||
*
|
||||
* @return {?string} Returns `null` if no errors are detected. Otherwise returns the error message.
|
||||
*/
|
||||
this.init = function () {
|
||||
|
||||
if ( renderer.capabilities.maxVertexTextures === 0 ) {
|
||||
|
||||
return 'No support for vertex shader textures.';
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.variables.length; i ++ ) {
|
||||
|
||||
const variable = this.variables[ i ];
|
||||
|
||||
// Creates rendertargets and initialize them with input texture
|
||||
variable.renderTargets[ 0 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
|
||||
variable.renderTargets[ 1 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
|
||||
this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 0 ] );
|
||||
this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 1 ] );
|
||||
|
||||
// Adds dependencies uniforms to the ShaderMaterial
|
||||
const material = variable.material;
|
||||
const uniforms = material.uniforms;
|
||||
|
||||
if ( variable.dependencies !== null ) {
|
||||
|
||||
for ( let d = 0; d < variable.dependencies.length; d ++ ) {
|
||||
|
||||
const depVar = variable.dependencies[ d ];
|
||||
|
||||
if ( depVar.name !== variable.name ) {
|
||||
|
||||
// Checks if variable exists
|
||||
let found = false;
|
||||
|
||||
for ( let j = 0; j < this.variables.length; j ++ ) {
|
||||
|
||||
if ( depVar.name === this.variables[ j ].name ) {
|
||||
|
||||
found = true;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( ! found ) {
|
||||
|
||||
return 'Variable dependency not found. Variable=' + variable.name + ', dependency=' + depVar.name;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uniforms[ depVar.name ] = { value: null };
|
||||
|
||||
material.fragmentShader = '\nuniform sampler2D ' + depVar.name + ';\n' + material.fragmentShader;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.currentTextureIndex = 0;
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the compute. This method is usually called in the animation loop.
|
||||
*/
|
||||
this.compute = function () {
|
||||
|
||||
const currentTextureIndex = this.currentTextureIndex;
|
||||
const nextTextureIndex = this.currentTextureIndex === 0 ? 1 : 0;
|
||||
|
||||
for ( let i = 0, il = this.variables.length; i < il; i ++ ) {
|
||||
|
||||
const variable = this.variables[ i ];
|
||||
|
||||
// Sets texture dependencies uniforms
|
||||
if ( variable.dependencies !== null ) {
|
||||
|
||||
const uniforms = variable.material.uniforms;
|
||||
|
||||
for ( let d = 0, dl = variable.dependencies.length; d < dl; d ++ ) {
|
||||
|
||||
const depVar = variable.dependencies[ d ];
|
||||
|
||||
uniforms[ depVar.name ].value = depVar.renderTargets[ currentTextureIndex ].texture;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Performs the computation for this variable
|
||||
this.doRenderTarget( variable.material, variable.renderTargets[ nextTextureIndex ] );
|
||||
|
||||
}
|
||||
|
||||
this.currentTextureIndex = nextTextureIndex;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current render target for the given compute variable.
|
||||
*
|
||||
* @param {Object} variable - The compute variable.
|
||||
* @return {WebGLRenderTarget} The current render target.
|
||||
*/
|
||||
this.getCurrentRenderTarget = function ( variable ) {
|
||||
|
||||
return variable.renderTargets[ this.currentTextureIndex ];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the alternate render target for the given compute variable.
|
||||
*
|
||||
* @param {Object} variable - The compute variable.
|
||||
* @return {WebGLRenderTarget} The alternate render target.
|
||||
*/
|
||||
this.getAlternateRenderTarget = function ( variable ) {
|
||||
|
||||
return variable.renderTargets[ this.currentTextureIndex === 0 ? 1 : 0 ];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Frees all internal resources. Call this method if you don't need the
|
||||
* renderer anymore.
|
||||
*/
|
||||
this.dispose = function () {
|
||||
|
||||
quad.dispose();
|
||||
|
||||
const variables = this.variables;
|
||||
|
||||
for ( let i = 0; i < variables.length; i ++ ) {
|
||||
|
||||
const variable = variables[ i ];
|
||||
|
||||
if ( variable.initialValueTexture ) variable.initialValueTexture.dispose();
|
||||
|
||||
const renderTargets = variable.renderTargets;
|
||||
|
||||
for ( let j = 0; j < renderTargets.length; j ++ ) {
|
||||
|
||||
const renderTarget = renderTargets[ j ];
|
||||
renderTarget.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function addResolutionDefine( materialShader ) {
|
||||
|
||||
materialShader.defines.resolution = 'vec2( ' + sizeX.toFixed( 1 ) + ', ' + sizeY.toFixed( 1 ) + ' )';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a resolution defined for the given material shader.
|
||||
*
|
||||
* @param {Object} materialShader - The material shader.
|
||||
*/
|
||||
this.addResolutionDefine = addResolutionDefine;
|
||||
|
||||
|
||||
// The following functions can be used to compute things manually
|
||||
|
||||
function createShaderMaterial( computeFragmentShader, uniforms ) {
|
||||
|
||||
uniforms = uniforms || {};
|
||||
|
||||
const material = new ShaderMaterial( {
|
||||
name: 'GPUComputationShader',
|
||||
uniforms: uniforms,
|
||||
vertexShader: getPassThroughVertexShader(),
|
||||
fragmentShader: computeFragmentShader
|
||||
} );
|
||||
|
||||
addResolutionDefine( material );
|
||||
|
||||
return material;
|
||||
|
||||
}
|
||||
|
||||
this.createShaderMaterial = createShaderMaterial;
|
||||
|
||||
/**
|
||||
* Creates a new render target from the given parameters.
|
||||
*
|
||||
* @param {number} sizeXTexture - The width of the render target.
|
||||
* @param {number} sizeYTexture - The height of the render target.
|
||||
* @param {number} wrapS - The wrapS value.
|
||||
* @param {number} wrapT - The wrapS value.
|
||||
* @param {number} minFilter - The minFilter value.
|
||||
* @param {number} magFilter - The magFilter value.
|
||||
* @return {WebGLRenderTarget} The new render target.
|
||||
*/
|
||||
this.createRenderTarget = function ( sizeXTexture, sizeYTexture, wrapS, wrapT, minFilter, magFilter ) {
|
||||
|
||||
sizeXTexture = sizeXTexture || sizeX;
|
||||
sizeYTexture = sizeYTexture || sizeY;
|
||||
|
||||
wrapS = wrapS || ClampToEdgeWrapping;
|
||||
wrapT = wrapT || ClampToEdgeWrapping;
|
||||
|
||||
minFilter = minFilter || NearestFilter;
|
||||
magFilter = magFilter || NearestFilter;
|
||||
|
||||
const renderTarget = new WebGLRenderTarget( sizeXTexture, sizeYTexture, {
|
||||
wrapS: wrapS,
|
||||
wrapT: wrapT,
|
||||
minFilter: minFilter,
|
||||
magFilter: magFilter,
|
||||
format: RGBAFormat,
|
||||
type: dataType,
|
||||
depthBuffer: false
|
||||
} );
|
||||
|
||||
return renderTarget;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new data texture.
|
||||
*
|
||||
* @return {DataTexture} The new data texture.
|
||||
*/
|
||||
this.createTexture = function () {
|
||||
|
||||
const data = new Float32Array( sizeX * sizeY * 4 );
|
||||
const texture = new DataTexture( data, sizeX, sizeY, RGBAFormat, FloatType );
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the given texture into the given render target.
|
||||
*
|
||||
* @param {Texture} input - The input.
|
||||
* @param {WebGLRenderTarget} output - The output.
|
||||
*/
|
||||
this.renderTexture = function ( input, output ) {
|
||||
|
||||
passThruUniforms.passThruTexture.value = input;
|
||||
|
||||
this.doRenderTarget( passThruShader, output );
|
||||
|
||||
passThruUniforms.passThruTexture.value = null;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the given material into the given render target
|
||||
* with a full-screen pass.
|
||||
*
|
||||
* @param {Material} material - The material.
|
||||
* @param {WebGLRenderTarget} output - The output.
|
||||
*/
|
||||
this.doRenderTarget = function ( material, output ) {
|
||||
|
||||
const currentRenderTarget = renderer.getRenderTarget();
|
||||
|
||||
const currentXrEnabled = renderer.xr.enabled;
|
||||
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
||||
|
||||
renderer.xr.enabled = false; // Avoid camera modification
|
||||
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
|
||||
quad.material = material;
|
||||
renderer.setRenderTarget( output );
|
||||
quad.render( renderer );
|
||||
quad.material = passThruShader;
|
||||
|
||||
renderer.xr.enabled = currentXrEnabled;
|
||||
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
|
||||
|
||||
renderer.setRenderTarget( currentRenderTarget );
|
||||
|
||||
};
|
||||
|
||||
// Shaders
|
||||
|
||||
function getPassThroughVertexShader() {
|
||||
|
||||
return 'void main() {\n' +
|
||||
'\n' +
|
||||
' gl_Position = vec4( position, 1.0 );\n' +
|
||||
'\n' +
|
||||
'}\n';
|
||||
|
||||
}
|
||||
|
||||
function getPassThroughFragmentShader() {
|
||||
|
||||
return 'uniform sampler2D passThruTexture;\n' +
|
||||
'\n' +
|
||||
'void main() {\n' +
|
||||
'\n' +
|
||||
' vec2 uv = gl_FragCoord.xy / resolution.xy;\n' +
|
||||
'\n' +
|
||||
' gl_FragColor = texture2D( passThruTexture, uv );\n' +
|
||||
'\n' +
|
||||
'}\n';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { GPUComputationRenderer };
|
||||
78
node_modules/three/examples/jsm/misc/Gyroscope.js
generated
vendored
Normal file
78
node_modules/three/examples/jsm/misc/Gyroscope.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Object3D,
|
||||
Quaternion,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
const _translationObject = new Vector3();
|
||||
const _quaternionObject = new Quaternion();
|
||||
const _scaleObject = new Vector3();
|
||||
|
||||
const _translationWorld = new Vector3();
|
||||
const _quaternionWorld = new Quaternion();
|
||||
const _scaleWorld = new Vector3();
|
||||
|
||||
/**
|
||||
* A special type of 3D object that takes a position from the scene graph hierarchy
|
||||
* but uses its local rotation as world rotation. It works like real-world gyroscope -
|
||||
* you can move it around using hierarchy while its orientation stays fixed with
|
||||
* respect to the world.
|
||||
*
|
||||
* @augments Object3D
|
||||
* @three_import import { Gyroscope } from 'three/addons/misc/Gyroscope.js';
|
||||
*/
|
||||
class Gyroscope extends Object3D {
|
||||
|
||||
/**
|
||||
* Constructs a new gyroscope.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
updateMatrixWorld( force ) {
|
||||
|
||||
this.matrixAutoUpdate && this.updateMatrix();
|
||||
|
||||
// update matrixWorld
|
||||
|
||||
if ( this.matrixWorldNeedsUpdate || force ) {
|
||||
|
||||
if ( this.parent !== null ) {
|
||||
|
||||
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
|
||||
|
||||
this.matrixWorld.decompose( _translationWorld, _quaternionWorld, _scaleWorld );
|
||||
this.matrix.decompose( _translationObject, _quaternionObject, _scaleObject );
|
||||
|
||||
this.matrixWorld.compose( _translationWorld, _quaternionObject, _scaleWorld );
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
this.matrixWorld.copy( this.matrix );
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.matrixWorldNeedsUpdate = false;
|
||||
|
||||
force = true;
|
||||
|
||||
}
|
||||
|
||||
// update children
|
||||
|
||||
for ( let i = 0, l = this.children.length; i < l; i ++ ) {
|
||||
|
||||
this.children[ i ].updateMatrixWorld( force );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Gyroscope };
|
||||
391
node_modules/three/examples/jsm/misc/MD2Character.js
generated
vendored
Normal file
391
node_modules/three/examples/jsm/misc/MD2Character.js
generated
vendored
Normal file
@@ -0,0 +1,391 @@
|
||||
import {
|
||||
AnimationMixer,
|
||||
Box3,
|
||||
Mesh,
|
||||
MeshLambertMaterial,
|
||||
Object3D,
|
||||
TextureLoader,
|
||||
UVMapping,
|
||||
SRGBColorSpace
|
||||
} from 'three';
|
||||
import { MD2Loader } from '../loaders/MD2Loader.js';
|
||||
|
||||
/**
|
||||
* This class represents a management component for animated MD2
|
||||
* character assets.
|
||||
*
|
||||
* @three_import import { MD2Character } from 'three/addons/misc/MD2Character.js';
|
||||
*/
|
||||
class MD2Character {
|
||||
|
||||
/**
|
||||
* Constructs a new MD2 character.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
/**
|
||||
* The mesh scale.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.scale = 1;
|
||||
|
||||
/**
|
||||
* The FPS
|
||||
*
|
||||
* @type {number}
|
||||
* @default 6
|
||||
*/
|
||||
this.animationFPS = 6;
|
||||
|
||||
/**
|
||||
* The root 3D object
|
||||
*
|
||||
* @type {Object3D}
|
||||
*/
|
||||
this.root = new Object3D();
|
||||
|
||||
/**
|
||||
* The body mesh.
|
||||
*
|
||||
* @type {?Mesh}
|
||||
* @default null
|
||||
*/
|
||||
this.meshBody = null;
|
||||
|
||||
/**
|
||||
* The weapon mesh.
|
||||
*
|
||||
* @type {?Mesh}
|
||||
* @default null
|
||||
*/
|
||||
this.meshWeapon = null;
|
||||
|
||||
/**
|
||||
* The body skins.
|
||||
*
|
||||
* @type {Array<Texture>}
|
||||
*/
|
||||
this.skinsBody = [];
|
||||
|
||||
/**
|
||||
* The weapon skins.
|
||||
*
|
||||
* @type {Array<Texture>}
|
||||
*/
|
||||
this.skinsWeapon = [];
|
||||
|
||||
/**
|
||||
* The weapon meshes.
|
||||
*
|
||||
* @type {Array<Mesh>}
|
||||
*/
|
||||
this.weapons = [];
|
||||
|
||||
/**
|
||||
* The name of the active animation clip.
|
||||
*
|
||||
* @type {?string}
|
||||
* @default null
|
||||
*/
|
||||
this.activeAnimationClipName = null;
|
||||
|
||||
/**
|
||||
* The animation mixer.
|
||||
*
|
||||
* @type {?AnimationMixer}
|
||||
* @default null
|
||||
*/
|
||||
this.mixer = null;
|
||||
|
||||
/**
|
||||
* The `onLoad` callback function.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.onLoadComplete = function () {};
|
||||
|
||||
// internal
|
||||
|
||||
this.loadCounter = 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the character model for the given config.
|
||||
*
|
||||
* @param {Object} config - The config which defines the model and textures paths.
|
||||
*/
|
||||
loadParts( config ) {
|
||||
|
||||
const scope = this;
|
||||
|
||||
function createPart( geometry, skinMap ) {
|
||||
|
||||
const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
|
||||
const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
|
||||
|
||||
//
|
||||
|
||||
const mesh = new Mesh( geometry, materialTexture );
|
||||
mesh.rotation.y = - Math.PI / 2;
|
||||
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
//
|
||||
|
||||
mesh.materialTexture = materialTexture;
|
||||
mesh.materialWireframe = materialWireframe;
|
||||
|
||||
return mesh;
|
||||
|
||||
}
|
||||
|
||||
function loadTextures( baseUrl, textureUrls ) {
|
||||
|
||||
const textureLoader = new TextureLoader();
|
||||
const textures = [];
|
||||
|
||||
for ( let i = 0; i < textureUrls.length; i ++ ) {
|
||||
|
||||
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
|
||||
textures[ i ].mapping = UVMapping;
|
||||
textures[ i ].name = textureUrls[ i ];
|
||||
textures[ i ].colorSpace = SRGBColorSpace;
|
||||
|
||||
}
|
||||
|
||||
return textures;
|
||||
|
||||
}
|
||||
|
||||
function checkLoadingComplete() {
|
||||
|
||||
scope.loadCounter -= 1;
|
||||
|
||||
if ( scope.loadCounter === 0 ) scope.onLoadComplete();
|
||||
|
||||
}
|
||||
|
||||
this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
|
||||
|
||||
const weaponsTextures = [];
|
||||
for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
|
||||
// SKINS
|
||||
|
||||
this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
|
||||
this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
|
||||
|
||||
// BODY
|
||||
|
||||
const loader = new MD2Loader();
|
||||
|
||||
loader.load( config.baseUrl + config.body, function ( geo ) {
|
||||
|
||||
const boundingBox = new Box3();
|
||||
boundingBox.setFromBufferAttribute( geo.attributes.position );
|
||||
|
||||
scope.root.position.y = - scope.scale * boundingBox.min.y;
|
||||
|
||||
const mesh = createPart( geo, scope.skinsBody[ 0 ] );
|
||||
mesh.scale.set( scope.scale, scope.scale, scope.scale );
|
||||
|
||||
scope.root.add( mesh );
|
||||
|
||||
scope.meshBody = mesh;
|
||||
|
||||
scope.meshBody.clipOffset = 0;
|
||||
scope.activeAnimationClipName = mesh.geometry.animations[ 0 ].name;
|
||||
|
||||
scope.mixer = new AnimationMixer( mesh );
|
||||
|
||||
checkLoadingComplete();
|
||||
|
||||
} );
|
||||
|
||||
// WEAPONS
|
||||
|
||||
const generateCallback = function ( index, name ) {
|
||||
|
||||
return function ( geo ) {
|
||||
|
||||
const mesh = createPart( geo, scope.skinsWeapon[ index ] );
|
||||
mesh.scale.set( scope.scale, scope.scale, scope.scale );
|
||||
mesh.visible = false;
|
||||
|
||||
mesh.name = name;
|
||||
|
||||
scope.root.add( mesh );
|
||||
|
||||
scope.weapons[ index ] = mesh;
|
||||
scope.meshWeapon = mesh;
|
||||
|
||||
checkLoadingComplete();
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
for ( let i = 0; i < config.weapons.length; i ++ ) {
|
||||
|
||||
loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback rate.
|
||||
*
|
||||
* @param {number} rate - The playback rate to set.
|
||||
*/
|
||||
setPlaybackRate( rate ) {
|
||||
|
||||
if ( rate !== 0 ) {
|
||||
|
||||
this.mixer.timeScale = 1 / rate;
|
||||
|
||||
} else {
|
||||
|
||||
this.mixer.timeScale = 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wireframe material flag.
|
||||
*
|
||||
* @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
|
||||
*/
|
||||
setWireframe( wireframeEnabled ) {
|
||||
|
||||
if ( wireframeEnabled ) {
|
||||
|
||||
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
|
||||
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
|
||||
|
||||
} else {
|
||||
|
||||
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
|
||||
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the skin defined by the given skin index. This will result in a different texture
|
||||
* for the body mesh.
|
||||
*
|
||||
* @param {number} index - The skin index.
|
||||
*/
|
||||
setSkin( index ) {
|
||||
|
||||
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
|
||||
|
||||
this.meshBody.material.map = this.skinsBody[ index ];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the weapon defined by the given weapon index. This will result in a different weapon
|
||||
* hold by the character.
|
||||
*
|
||||
* @param {number} index - The weapon index.
|
||||
*/
|
||||
setWeapon( index ) {
|
||||
|
||||
for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
|
||||
|
||||
const activeWeapon = this.weapons[ index ];
|
||||
|
||||
if ( activeWeapon ) {
|
||||
|
||||
activeWeapon.visible = true;
|
||||
this.meshWeapon = activeWeapon;
|
||||
|
||||
this.syncWeaponAnimation();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the defined animation clip as the active animation.
|
||||
*
|
||||
* @param {string} clipName - The name of the animation clip.
|
||||
*/
|
||||
setAnimation( clipName ) {
|
||||
|
||||
if ( this.meshBody ) {
|
||||
|
||||
if ( this.meshBody.activeAction ) {
|
||||
|
||||
this.meshBody.activeAction.stop();
|
||||
this.meshBody.activeAction = null;
|
||||
|
||||
}
|
||||
|
||||
const action = this.mixer.clipAction( clipName, this.meshBody );
|
||||
|
||||
if ( action ) {
|
||||
|
||||
this.meshBody.activeAction = action.play();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.activeClipName = clipName;
|
||||
|
||||
this.syncWeaponAnimation();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes the weapon with the body animation.
|
||||
*/
|
||||
syncWeaponAnimation() {
|
||||
|
||||
const clipName = this.activeClipName;
|
||||
|
||||
if ( this.meshWeapon ) {
|
||||
|
||||
if ( this.meshWeapon.activeAction ) {
|
||||
|
||||
this.meshWeapon.activeAction.stop();
|
||||
this.meshWeapon.activeAction = null;
|
||||
|
||||
}
|
||||
|
||||
const action = this.mixer.clipAction( clipName, this.meshWeapon );
|
||||
|
||||
if ( action ) {
|
||||
|
||||
this.meshWeapon.activeAction = action.syncWith( this.meshBody.activeAction ).play();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the animations of the mesh. Must be called inside the animation loop.
|
||||
*
|
||||
* @param {number} delta - The delta time in seconds.
|
||||
*/
|
||||
update( delta ) {
|
||||
|
||||
if ( this.mixer ) this.mixer.update( delta );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MD2Character };
|
||||
740
node_modules/three/examples/jsm/misc/MD2CharacterComplex.js
generated
vendored
Normal file
740
node_modules/three/examples/jsm/misc/MD2CharacterComplex.js
generated
vendored
Normal file
@@ -0,0 +1,740 @@
|
||||
import {
|
||||
Box3,
|
||||
MathUtils,
|
||||
MeshLambertMaterial,
|
||||
Object3D,
|
||||
TextureLoader,
|
||||
UVMapping,
|
||||
SRGBColorSpace
|
||||
} from 'three';
|
||||
import { MD2Loader } from '../loaders/MD2Loader.js';
|
||||
import { MorphBlendMesh } from '../misc/MorphBlendMesh.js';
|
||||
|
||||
/**
|
||||
* This class represents a management component for animated MD2
|
||||
* character assets. It provides a larger API compared to {@link MD2Character}.
|
||||
*
|
||||
* @three_import import { MD2CharacterComplex } from 'three/addons/misc/MD2CharacterComplex.js';
|
||||
*/
|
||||
class MD2CharacterComplex {
|
||||
|
||||
/**
|
||||
* Constructs a new MD2 character.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
/**
|
||||
* The mesh scale.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.scale = 1;
|
||||
|
||||
/**
|
||||
* The FPS
|
||||
*
|
||||
* @type {number}
|
||||
* @default 6
|
||||
*/
|
||||
this.animationFPS = 6;
|
||||
|
||||
/**
|
||||
* The transition frames.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 15
|
||||
*/
|
||||
this.transitionFrames = 15;
|
||||
|
||||
/**
|
||||
* The character's maximum speed.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 275
|
||||
*/
|
||||
this.maxSpeed = 275;
|
||||
|
||||
/**
|
||||
* The character's maximum reverse speed.
|
||||
*
|
||||
* @type {number}
|
||||
* @default - 275
|
||||
*/
|
||||
this.maxReverseSpeed = - 275;
|
||||
|
||||
/**
|
||||
* The character's front acceleration.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 600
|
||||
*/
|
||||
this.frontAcceleration = 600;
|
||||
|
||||
/**
|
||||
* The character's back acceleration.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 600
|
||||
*/
|
||||
this.backAcceleration = 600;
|
||||
|
||||
/**
|
||||
* The character's front deceleration.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 600
|
||||
*/
|
||||
this.frontDeceleration = 600;
|
||||
|
||||
/**
|
||||
* The character's angular speed.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 2.5
|
||||
*/
|
||||
this.angularSpeed = 2.5;
|
||||
|
||||
/**
|
||||
* The root 3D object
|
||||
*
|
||||
* @type {Object3D}
|
||||
*/
|
||||
this.root = new Object3D();
|
||||
|
||||
/**
|
||||
* The body mesh.
|
||||
*
|
||||
* @type {?Mesh}
|
||||
* @default null
|
||||
*/
|
||||
this.meshBody = null;
|
||||
|
||||
/**
|
||||
* The weapon mesh.
|
||||
*
|
||||
* @type {?Mesh}
|
||||
* @default null
|
||||
*/
|
||||
this.meshWeapon = null;
|
||||
|
||||
/**
|
||||
* The movement controls.
|
||||
*
|
||||
* @type {?Object}
|
||||
* @default null
|
||||
*/
|
||||
this.controls = null;
|
||||
|
||||
/**
|
||||
* The body skins.
|
||||
*
|
||||
* @type {Array<Texture>}
|
||||
*/
|
||||
this.skinsBody = [];
|
||||
|
||||
/**
|
||||
* The weapon skins.
|
||||
*
|
||||
* @type {Array<Texture>}
|
||||
*/
|
||||
this.skinsWeapon = [];
|
||||
|
||||
/**
|
||||
* The weapon meshes.
|
||||
*
|
||||
* @type {Array<Mesh>}
|
||||
*/
|
||||
this.weapons = [];
|
||||
|
||||
/**
|
||||
* The current skin.
|
||||
*
|
||||
* @type {Texture}
|
||||
* @default undefined
|
||||
*/
|
||||
this.currentSkin = undefined;
|
||||
|
||||
//
|
||||
|
||||
this.onLoadComplete = function () {};
|
||||
|
||||
// internals
|
||||
|
||||
this.meshes = [];
|
||||
this.animations = {};
|
||||
|
||||
this.loadCounter = 0;
|
||||
|
||||
// internal movement control variables
|
||||
|
||||
this.speed = 0;
|
||||
this.bodyOrientation = 0;
|
||||
|
||||
this.walkSpeed = this.maxSpeed;
|
||||
this.crouchSpeed = this.maxSpeed * 0.5;
|
||||
|
||||
// internal animation parameters
|
||||
|
||||
this.activeAnimation = null;
|
||||
this.oldAnimation = null;
|
||||
|
||||
// API
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles shadow casting and receiving on the character's meshes.
|
||||
*
|
||||
* @param {boolean} enable - Whether to enable shadows or not.
|
||||
*/
|
||||
enableShadows( enable ) {
|
||||
|
||||
for ( let i = 0; i < this.meshes.length; i ++ ) {
|
||||
|
||||
this.meshes[ i ].castShadow = enable;
|
||||
this.meshes[ i ].receiveShadow = enable;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility on the character's meshes.
|
||||
*
|
||||
* @param {boolean} enable - Whether the character is visible or not.
|
||||
*/
|
||||
setVisible( enable ) {
|
||||
|
||||
for ( let i = 0; i < this.meshes.length; i ++ ) {
|
||||
|
||||
this.meshes[ i ].visible = enable;
|
||||
this.meshes[ i ].visible = enable;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares certain resources from a different character model.
|
||||
*
|
||||
* @param {MD2CharacterComplex} original - The original MD2 character.
|
||||
*/
|
||||
shareParts( original ) {
|
||||
|
||||
this.animations = original.animations;
|
||||
this.walkSpeed = original.walkSpeed;
|
||||
this.crouchSpeed = original.crouchSpeed;
|
||||
|
||||
this.skinsBody = original.skinsBody;
|
||||
this.skinsWeapon = original.skinsWeapon;
|
||||
|
||||
// BODY
|
||||
|
||||
const mesh = this._createPart( original.meshBody.geometry, this.skinsBody[ 0 ] );
|
||||
mesh.scale.set( this.scale, this.scale, this.scale );
|
||||
|
||||
this.root.position.y = original.root.position.y;
|
||||
this.root.add( mesh );
|
||||
|
||||
this.meshBody = mesh;
|
||||
|
||||
this.meshes.push( mesh );
|
||||
|
||||
// WEAPONS
|
||||
|
||||
for ( let i = 0; i < original.weapons.length; i ++ ) {
|
||||
|
||||
const meshWeapon = this._createPart( original.weapons[ i ].geometry, this.skinsWeapon[ i ] );
|
||||
meshWeapon.scale.set( this.scale, this.scale, this.scale );
|
||||
meshWeapon.visible = false;
|
||||
|
||||
meshWeapon.name = original.weapons[ i ].name;
|
||||
|
||||
this.root.add( meshWeapon );
|
||||
|
||||
this.weapons[ i ] = meshWeapon;
|
||||
this.meshWeapon = meshWeapon;
|
||||
|
||||
this.meshes.push( meshWeapon );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the character model for the given config.
|
||||
*
|
||||
* @param {Object} config - The config which defines the model and textures paths.
|
||||
*/
|
||||
loadParts( config ) {
|
||||
|
||||
const scope = this;
|
||||
|
||||
function loadTextures( baseUrl, textureUrls ) {
|
||||
|
||||
const textureLoader = new TextureLoader();
|
||||
const textures = [];
|
||||
|
||||
for ( let i = 0; i < textureUrls.length; i ++ ) {
|
||||
|
||||
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
|
||||
textures[ i ].mapping = UVMapping;
|
||||
textures[ i ].name = textureUrls[ i ];
|
||||
textures[ i ].colorSpace = SRGBColorSpace;
|
||||
|
||||
}
|
||||
|
||||
return textures;
|
||||
|
||||
}
|
||||
|
||||
function checkLoadingComplete() {
|
||||
|
||||
scope.loadCounter -= 1;
|
||||
if ( scope.loadCounter === 0 ) scope.onLoadComplete();
|
||||
|
||||
}
|
||||
|
||||
this.animations = config.animations;
|
||||
this.walkSpeed = config.walkSpeed;
|
||||
this.crouchSpeed = config.crouchSpeed;
|
||||
|
||||
this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
|
||||
|
||||
const weaponsTextures = [];
|
||||
for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
|
||||
|
||||
// SKINS
|
||||
|
||||
this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
|
||||
this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
|
||||
|
||||
// BODY
|
||||
|
||||
const loader = new MD2Loader();
|
||||
|
||||
loader.load( config.baseUrl + config.body, function ( geo ) {
|
||||
|
||||
const boundingBox = new Box3();
|
||||
boundingBox.setFromBufferAttribute( geo.attributes.position );
|
||||
|
||||
scope.root.position.y = - scope.scale * boundingBox.min.y;
|
||||
|
||||
const mesh = scope._createPart( geo, scope.skinsBody[ 0 ] );
|
||||
mesh.scale.set( scope.scale, scope.scale, scope.scale );
|
||||
|
||||
scope.root.add( mesh );
|
||||
|
||||
scope.meshBody = mesh;
|
||||
scope.meshes.push( mesh );
|
||||
|
||||
checkLoadingComplete();
|
||||
|
||||
} );
|
||||
|
||||
// WEAPONS
|
||||
|
||||
const generateCallback = function ( index, name ) {
|
||||
|
||||
return function ( geo ) {
|
||||
|
||||
const mesh = scope._createPart( geo, scope.skinsWeapon[ index ] );
|
||||
mesh.scale.set( scope.scale, scope.scale, scope.scale );
|
||||
mesh.visible = false;
|
||||
|
||||
mesh.name = name;
|
||||
|
||||
scope.root.add( mesh );
|
||||
|
||||
scope.weapons[ index ] = mesh;
|
||||
scope.meshWeapon = mesh;
|
||||
scope.meshes.push( mesh );
|
||||
|
||||
checkLoadingComplete();
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
for ( let i = 0; i < config.weapons.length; i ++ ) {
|
||||
|
||||
loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback rate.
|
||||
*
|
||||
* @param {number} rate - The playback rate to set.
|
||||
*/
|
||||
setPlaybackRate( rate ) {
|
||||
|
||||
if ( this.meshBody ) this.meshBody.duration = this.meshBody.baseDuration / rate;
|
||||
if ( this.meshWeapon ) this.meshWeapon.duration = this.meshWeapon.baseDuration / rate;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wireframe material flag.
|
||||
*
|
||||
* @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
|
||||
*/
|
||||
setWireframe( wireframeEnabled ) {
|
||||
|
||||
if ( wireframeEnabled ) {
|
||||
|
||||
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
|
||||
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
|
||||
|
||||
} else {
|
||||
|
||||
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
|
||||
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the skin defined by the given skin index. This will result in a different texture
|
||||
* for the body mesh.
|
||||
*
|
||||
* @param {number} index - The skin index.
|
||||
*/
|
||||
setSkin( index ) {
|
||||
|
||||
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
|
||||
|
||||
this.meshBody.material.map = this.skinsBody[ index ];
|
||||
this.currentSkin = index;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the weapon defined by the given weapon index. This will result in a different weapon
|
||||
* hold by the character.
|
||||
*
|
||||
* @param {number} index - The weapon index.
|
||||
*/
|
||||
setWeapon( index ) {
|
||||
|
||||
for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
|
||||
|
||||
const activeWeapon = this.weapons[ index ];
|
||||
|
||||
if ( activeWeapon ) {
|
||||
|
||||
activeWeapon.visible = true;
|
||||
this.meshWeapon = activeWeapon;
|
||||
|
||||
if ( this.activeAnimation ) {
|
||||
|
||||
activeWeapon.playAnimation( this.activeAnimation );
|
||||
this.meshWeapon.setAnimationTime( this.activeAnimation, this.meshBody.getAnimationTime( this.activeAnimation ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the defined animation clip as the active animation.
|
||||
*
|
||||
* @param {string} animationName - The name of the animation clip.
|
||||
*/
|
||||
setAnimation( animationName ) {
|
||||
|
||||
if ( animationName === this.activeAnimation || ! animationName ) return;
|
||||
|
||||
if ( this.meshBody ) {
|
||||
|
||||
this.meshBody.setAnimationWeight( animationName, 0 );
|
||||
this.meshBody.playAnimation( animationName );
|
||||
|
||||
this.oldAnimation = this.activeAnimation;
|
||||
this.activeAnimation = animationName;
|
||||
|
||||
this.blendCounter = this.transitionFrames;
|
||||
|
||||
}
|
||||
|
||||
if ( this.meshWeapon ) {
|
||||
|
||||
this.meshWeapon.setAnimationWeight( animationName, 0 );
|
||||
this.meshWeapon.playAnimation( animationName );
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
update( delta ) {
|
||||
|
||||
if ( this.controls ) this.updateMovementModel( delta );
|
||||
|
||||
if ( this.animations ) {
|
||||
|
||||
this.updateBehaviors();
|
||||
this.updateAnimations( delta );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the animations of the mesh. Must be called inside the animation loop.
|
||||
*
|
||||
* @param {number} delta - The delta time in seconds.
|
||||
*/
|
||||
updateAnimations( delta ) {
|
||||
|
||||
let mix = 1;
|
||||
|
||||
if ( this.blendCounter > 0 ) {
|
||||
|
||||
mix = ( this.transitionFrames - this.blendCounter ) / this.transitionFrames;
|
||||
this.blendCounter -= 1;
|
||||
|
||||
}
|
||||
|
||||
if ( this.meshBody ) {
|
||||
|
||||
this.meshBody.update( delta );
|
||||
|
||||
this.meshBody.setAnimationWeight( this.activeAnimation, mix );
|
||||
this.meshBody.setAnimationWeight( this.oldAnimation, 1 - mix );
|
||||
|
||||
}
|
||||
|
||||
if ( this.meshWeapon ) {
|
||||
|
||||
this.meshWeapon.update( delta );
|
||||
|
||||
this.meshWeapon.setAnimationWeight( this.activeAnimation, mix );
|
||||
this.meshWeapon.setAnimationWeight( this.oldAnimation, 1 - mix );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the animation state based on the control inputs.
|
||||
*/
|
||||
updateBehaviors() {
|
||||
|
||||
const controls = this.controls;
|
||||
const animations = this.animations;
|
||||
|
||||
let moveAnimation, idleAnimation;
|
||||
|
||||
// crouch vs stand
|
||||
|
||||
if ( controls.crouch ) {
|
||||
|
||||
moveAnimation = animations[ 'crouchMove' ];
|
||||
idleAnimation = animations[ 'crouchIdle' ];
|
||||
|
||||
} else {
|
||||
|
||||
moveAnimation = animations[ 'move' ];
|
||||
idleAnimation = animations[ 'idle' ];
|
||||
|
||||
}
|
||||
|
||||
// actions
|
||||
|
||||
if ( controls.jump ) {
|
||||
|
||||
moveAnimation = animations[ 'jump' ];
|
||||
idleAnimation = animations[ 'jump' ];
|
||||
|
||||
}
|
||||
|
||||
if ( controls.attack ) {
|
||||
|
||||
if ( controls.crouch ) {
|
||||
|
||||
moveAnimation = animations[ 'crouchAttack' ];
|
||||
idleAnimation = animations[ 'crouchAttack' ];
|
||||
|
||||
} else {
|
||||
|
||||
moveAnimation = animations[ 'attack' ];
|
||||
idleAnimation = animations[ 'attack' ];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// set animations
|
||||
|
||||
if ( controls.moveForward || controls.moveBackward || controls.moveLeft || controls.moveRight ) {
|
||||
|
||||
if ( this.activeAnimation !== moveAnimation ) {
|
||||
|
||||
this.setAnimation( moveAnimation );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if ( Math.abs( this.speed ) < 0.2 * this.maxSpeed && ! ( controls.moveLeft || controls.moveRight || controls.moveForward || controls.moveBackward ) ) {
|
||||
|
||||
if ( this.activeAnimation !== idleAnimation ) {
|
||||
|
||||
this.setAnimation( idleAnimation );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// set animation direction
|
||||
|
||||
if ( controls.moveForward ) {
|
||||
|
||||
if ( this.meshBody ) {
|
||||
|
||||
this.meshBody.setAnimationDirectionForward( this.activeAnimation );
|
||||
this.meshBody.setAnimationDirectionForward( this.oldAnimation );
|
||||
|
||||
}
|
||||
|
||||
if ( this.meshWeapon ) {
|
||||
|
||||
this.meshWeapon.setAnimationDirectionForward( this.activeAnimation );
|
||||
this.meshWeapon.setAnimationDirectionForward( this.oldAnimation );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( controls.moveBackward ) {
|
||||
|
||||
if ( this.meshBody ) {
|
||||
|
||||
this.meshBody.setAnimationDirectionBackward( this.activeAnimation );
|
||||
this.meshBody.setAnimationDirectionBackward( this.oldAnimation );
|
||||
|
||||
}
|
||||
|
||||
if ( this.meshWeapon ) {
|
||||
|
||||
this.meshWeapon.setAnimationDirectionBackward( this.activeAnimation );
|
||||
this.meshWeapon.setAnimationDirectionBackward( this.oldAnimation );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the character model based on the control input.
|
||||
*
|
||||
* @param {number} delta - The delta time in seconds.
|
||||
*/
|
||||
updateMovementModel( delta ) {
|
||||
|
||||
function exponentialEaseOut( k ) {
|
||||
|
||||
return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1;
|
||||
|
||||
}
|
||||
|
||||
const controls = this.controls;
|
||||
|
||||
// speed based on controls
|
||||
|
||||
if ( controls.crouch ) this.maxSpeed = this.crouchSpeed;
|
||||
else this.maxSpeed = this.walkSpeed;
|
||||
|
||||
this.maxReverseSpeed = - this.maxSpeed;
|
||||
|
||||
if ( controls.moveForward ) this.speed = MathUtils.clamp( this.speed + delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
|
||||
if ( controls.moveBackward ) this.speed = MathUtils.clamp( this.speed - delta * this.backAcceleration, this.maxReverseSpeed, this.maxSpeed );
|
||||
|
||||
// orientation based on controls
|
||||
// (don't just stand while turning)
|
||||
|
||||
const dir = 1;
|
||||
|
||||
if ( controls.moveLeft ) {
|
||||
|
||||
this.bodyOrientation += delta * this.angularSpeed;
|
||||
this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
|
||||
|
||||
}
|
||||
|
||||
if ( controls.moveRight ) {
|
||||
|
||||
this.bodyOrientation -= delta * this.angularSpeed;
|
||||
this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
|
||||
|
||||
}
|
||||
|
||||
// speed decay
|
||||
|
||||
if ( ! ( controls.moveForward || controls.moveBackward ) ) {
|
||||
|
||||
if ( this.speed > 0 ) {
|
||||
|
||||
const k = exponentialEaseOut( this.speed / this.maxSpeed );
|
||||
this.speed = MathUtils.clamp( this.speed - k * delta * this.frontDeceleration, 0, this.maxSpeed );
|
||||
|
||||
} else {
|
||||
|
||||
const k = exponentialEaseOut( this.speed / this.maxReverseSpeed );
|
||||
this.speed = MathUtils.clamp( this.speed + k * delta * this.backAcceleration, this.maxReverseSpeed, 0 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// displacement
|
||||
|
||||
const forwardDelta = this.speed * delta;
|
||||
|
||||
this.root.position.x += Math.sin( this.bodyOrientation ) * forwardDelta;
|
||||
this.root.position.z += Math.cos( this.bodyOrientation ) * forwardDelta;
|
||||
|
||||
// steering
|
||||
|
||||
this.root.rotation.y = this.bodyOrientation;
|
||||
|
||||
}
|
||||
|
||||
// internal
|
||||
|
||||
_createPart( geometry, skinMap ) {
|
||||
|
||||
const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
|
||||
const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
|
||||
|
||||
//
|
||||
|
||||
const mesh = new MorphBlendMesh( geometry, materialTexture );
|
||||
mesh.rotation.y = - Math.PI / 2;
|
||||
|
||||
//
|
||||
|
||||
mesh.materialTexture = materialTexture;
|
||||
mesh.materialWireframe = materialWireframe;
|
||||
|
||||
//
|
||||
|
||||
mesh.autoCreateAnimations( this.animationFPS );
|
||||
|
||||
return mesh;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MD2CharacterComplex };
|
||||
119
node_modules/three/examples/jsm/misc/MorphAnimMesh.js
generated
vendored
Normal file
119
node_modules/three/examples/jsm/misc/MorphAnimMesh.js
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
Mesh
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* A special type of an animated mesh with a simple interface
|
||||
* for animation playback. It allows to playback just one animation
|
||||
* without any transitions or fading between animation changes.
|
||||
*
|
||||
* @augments Mesh
|
||||
* @three_import import { MorphAnimMesh } from 'three/addons/misc/MorphAnimMesh.js';
|
||||
*/
|
||||
class MorphAnimMesh extends Mesh {
|
||||
|
||||
/**
|
||||
* Constructs a new morph anim mesh.
|
||||
*
|
||||
* @param {BufferGeometry} [geometry] - The mesh geometry.
|
||||
* @param {Material|Array<Material>} [material] - The mesh material.
|
||||
*/
|
||||
constructor( geometry, material ) {
|
||||
|
||||
super( geometry, material );
|
||||
|
||||
this.type = 'MorphAnimMesh';
|
||||
|
||||
/**
|
||||
* The internal animation mixer.
|
||||
*
|
||||
* @type {AnimationMixer}
|
||||
*/
|
||||
this.mixer = new AnimationMixer( this );
|
||||
|
||||
/**
|
||||
* The current active animation action.
|
||||
*
|
||||
* @type {?AnimationAction}
|
||||
* @default null
|
||||
*/
|
||||
this.activeAction = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback direction to "forward".
|
||||
*/
|
||||
setDirectionForward() {
|
||||
|
||||
this.mixer.timeScale = 1.0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback direction to "backward".
|
||||
*/
|
||||
setDirectionBackward() {
|
||||
|
||||
this.mixer.timeScale = - 1.0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the defined animation clip. The implementation assumes the animation
|
||||
* clips are stored in {@link Object3D#animations} or the geometry.
|
||||
*
|
||||
* @param {string} label - The name of the animation clip.
|
||||
* @param {number} fps - The FPS of the animation clip.
|
||||
*/
|
||||
playAnimation( label, fps ) {
|
||||
|
||||
if ( this.activeAction ) {
|
||||
|
||||
this.activeAction.stop();
|
||||
this.activeAction = null;
|
||||
|
||||
}
|
||||
|
||||
const clip = AnimationClip.findByName( this, label );
|
||||
|
||||
if ( clip ) {
|
||||
|
||||
const action = this.mixer.clipAction( clip );
|
||||
action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
|
||||
this.activeAction = action.play();
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error( 'THREE.MorphAnimMesh: animations[' + label + '] undefined in .playAnimation()' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the animations of the mesh. Must be called inside the animation loop.
|
||||
*
|
||||
* @param {number} delta - The delta time in seconds.
|
||||
*/
|
||||
updateAnimation( delta ) {
|
||||
|
||||
this.mixer.update( delta );
|
||||
|
||||
}
|
||||
|
||||
copy( source, recursive ) {
|
||||
|
||||
super.copy( source, recursive );
|
||||
|
||||
this.mixer = new AnimationMixer( this );
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MorphAnimMesh };
|
||||
425
node_modules/three/examples/jsm/misc/MorphBlendMesh.js
generated
vendored
Normal file
425
node_modules/three/examples/jsm/misc/MorphBlendMesh.js
generated
vendored
Normal file
@@ -0,0 +1,425 @@
|
||||
import {
|
||||
MathUtils,
|
||||
Mesh
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* A special type of an animated mesh with a more advanced interface
|
||||
* for animation playback. Unlike {@link MorphAnimMesh}. It allows to
|
||||
* playback more than one morph animation at the same time but without
|
||||
* fading options.
|
||||
*
|
||||
* @augments Mesh
|
||||
* @three_import import { MorphBlendMesh } from 'three/addons/misc/MorphBlendMesh.js';
|
||||
*/
|
||||
class MorphBlendMesh extends Mesh {
|
||||
|
||||
/**
|
||||
* Constructs a new morph blend mesh.
|
||||
*
|
||||
* @param {BufferGeometry} [geometry] - The mesh geometry.
|
||||
* @param {Material|Array<Material>} [material] - The mesh material.
|
||||
*/
|
||||
constructor( geometry, material ) {
|
||||
|
||||
super( geometry, material );
|
||||
|
||||
/**
|
||||
* A dictionary of animations.
|
||||
*
|
||||
* @type {Object<string,Object>}
|
||||
*/
|
||||
this.animationsMap = {};
|
||||
|
||||
/**
|
||||
* A list of animations.
|
||||
*
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
this.animationsList = [];
|
||||
|
||||
// prepare default animation
|
||||
// (all frames played together in 1 second)
|
||||
|
||||
const numFrames = Object.keys( this.morphTargetDictionary ).length;
|
||||
|
||||
const name = '__default';
|
||||
|
||||
const startFrame = 0;
|
||||
const endFrame = numFrames - 1;
|
||||
|
||||
const fps = numFrames / 1;
|
||||
|
||||
this.createAnimation( name, startFrame, endFrame, fps );
|
||||
this.setAnimationWeight( name, 1 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @param {number} start - The start time.
|
||||
* @param {number} end - The end time.
|
||||
* @param {number} fps - The FPS.
|
||||
*/
|
||||
createAnimation( name, start, end, fps ) {
|
||||
|
||||
const animation = {
|
||||
|
||||
start: start,
|
||||
end: end,
|
||||
|
||||
length: end - start + 1,
|
||||
|
||||
fps: fps,
|
||||
duration: ( end - start ) / fps,
|
||||
|
||||
lastFrame: 0,
|
||||
currentFrame: 0,
|
||||
|
||||
active: false,
|
||||
|
||||
time: 0,
|
||||
direction: 1,
|
||||
weight: 1,
|
||||
|
||||
directionBackwards: false,
|
||||
mirroredLoop: false
|
||||
|
||||
};
|
||||
|
||||
this.animationsMap[ name ] = animation;
|
||||
this.animationsList.push( animation );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically creates animations based on the values in
|
||||
* {@link Mesh#morphTargetDictionary}.
|
||||
*
|
||||
* @param {number} fps - The FPS of all animations.
|
||||
*/
|
||||
autoCreateAnimations( fps ) {
|
||||
|
||||
const pattern = /([a-z]+)_?(\d+)/i;
|
||||
|
||||
let firstAnimation;
|
||||
|
||||
const frameRanges = {};
|
||||
|
||||
let i = 0;
|
||||
|
||||
for ( const key in this.morphTargetDictionary ) {
|
||||
|
||||
const chunks = key.match( pattern );
|
||||
|
||||
if ( chunks && chunks.length > 1 ) {
|
||||
|
||||
const name = chunks[ 1 ];
|
||||
|
||||
if ( ! frameRanges[ name ] ) frameRanges[ name ] = { start: Infinity, end: - Infinity };
|
||||
|
||||
const range = frameRanges[ name ];
|
||||
|
||||
if ( i < range.start ) range.start = i;
|
||||
if ( i > range.end ) range.end = i;
|
||||
|
||||
if ( ! firstAnimation ) firstAnimation = name;
|
||||
|
||||
}
|
||||
|
||||
i ++;
|
||||
|
||||
}
|
||||
|
||||
for ( const name in frameRanges ) {
|
||||
|
||||
const range = frameRanges[ name ];
|
||||
this.createAnimation( name, range.start, range.end, fps );
|
||||
|
||||
}
|
||||
|
||||
this.firstAnimation = firstAnimation;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback direction to "forward" for the
|
||||
* defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
*/
|
||||
setAnimationDirectionForward( name ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.direction = 1;
|
||||
animation.directionBackwards = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation playback direction to "backward" for the
|
||||
* defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
*/
|
||||
setAnimationDirectionBackward( name ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.direction = - 1;
|
||||
animation.directionBackwards = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FPS to the given value for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @param {number} fps - The FPS to set.
|
||||
*/
|
||||
setAnimationFPS( name, fps ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.fps = fps;
|
||||
animation.duration = ( animation.end - animation.start ) / animation.fps;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration to the given value for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @param {number} duration - The duration to set.
|
||||
*/
|
||||
setAnimationDuration( name, duration ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.duration = duration;
|
||||
animation.fps = ( animation.end - animation.start ) / animation.duration;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the weight to the given value for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @param {number} weight - The weight to set.
|
||||
*/
|
||||
setAnimationWeight( name, weight ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.weight = weight;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time to the given value for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @param {number} time - The time to set.
|
||||
*/
|
||||
setAnimationTime( name, time ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.time = time;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @return {number} The time.
|
||||
*/
|
||||
getAnimationTime( name ) {
|
||||
|
||||
let time = 0;
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
time = animation.time;
|
||||
|
||||
}
|
||||
|
||||
return time;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration for the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
* @return {number} The duration.
|
||||
*/
|
||||
getAnimationDuration( name ) {
|
||||
|
||||
let duration = - 1;
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
duration = animation.duration;
|
||||
|
||||
}
|
||||
|
||||
return duration;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
*/
|
||||
playAnimation( name ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.time = 0;
|
||||
animation.active = true;
|
||||
|
||||
} else {
|
||||
|
||||
console.warn( 'THREE.MorphBlendMesh: animation[' + name + '] undefined in .playAnimation()' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the defined animation.
|
||||
*
|
||||
* @param {string} name - The animation name.
|
||||
*/
|
||||
stopAnimation( name ) {
|
||||
|
||||
const animation = this.animationsMap[ name ];
|
||||
|
||||
if ( animation ) {
|
||||
|
||||
animation.active = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the animations of the mesh.
|
||||
*
|
||||
* @param {number} delta - The delta time in seconds.
|
||||
*/
|
||||
update( delta ) {
|
||||
|
||||
for ( let i = 0, il = this.animationsList.length; i < il; i ++ ) {
|
||||
|
||||
const animation = this.animationsList[ i ];
|
||||
|
||||
if ( ! animation.active ) continue;
|
||||
|
||||
const frameTime = animation.duration / animation.length;
|
||||
|
||||
animation.time += animation.direction * delta;
|
||||
|
||||
if ( animation.mirroredLoop ) {
|
||||
|
||||
if ( animation.time > animation.duration || animation.time < 0 ) {
|
||||
|
||||
animation.direction *= - 1;
|
||||
|
||||
if ( animation.time > animation.duration ) {
|
||||
|
||||
animation.time = animation.duration;
|
||||
animation.directionBackwards = true;
|
||||
|
||||
}
|
||||
|
||||
if ( animation.time < 0 ) {
|
||||
|
||||
animation.time = 0;
|
||||
animation.directionBackwards = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
animation.time = animation.time % animation.duration;
|
||||
|
||||
if ( animation.time < 0 ) animation.time += animation.duration;
|
||||
|
||||
}
|
||||
|
||||
const keyframe = animation.start + MathUtils.clamp( Math.floor( animation.time / frameTime ), 0, animation.length - 1 );
|
||||
const weight = animation.weight;
|
||||
|
||||
if ( keyframe !== animation.currentFrame ) {
|
||||
|
||||
this.morphTargetInfluences[ animation.lastFrame ] = 0;
|
||||
this.morphTargetInfluences[ animation.currentFrame ] = 1 * weight;
|
||||
|
||||
this.morphTargetInfluences[ keyframe ] = 0;
|
||||
|
||||
animation.lastFrame = animation.currentFrame;
|
||||
animation.currentFrame = keyframe;
|
||||
|
||||
}
|
||||
|
||||
let mix = ( animation.time % frameTime ) / frameTime;
|
||||
|
||||
if ( animation.directionBackwards ) mix = 1 - mix;
|
||||
|
||||
if ( animation.currentFrame !== animation.lastFrame ) {
|
||||
|
||||
this.morphTargetInfluences[ animation.currentFrame ] = mix * weight;
|
||||
this.morphTargetInfluences[ animation.lastFrame ] = ( 1 - mix ) * weight;
|
||||
|
||||
} else {
|
||||
|
||||
this.morphTargetInfluences[ animation.currentFrame ] = weight;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MorphBlendMesh };
|
||||
370
node_modules/three/examples/jsm/misc/ProgressiveLightMap.js
generated
vendored
Normal file
370
node_modules/three/examples/jsm/misc/ProgressiveLightMap.js
generated
vendored
Normal file
@@ -0,0 +1,370 @@
|
||||
import { DoubleSide, FloatType, HalfFloatType, Mesh, MeshBasicMaterial, MeshPhongMaterial, PlaneGeometry, Scene, WebGLRenderTarget } from 'three';
|
||||
import { potpack } from '../libs/potpack.module.js';
|
||||
|
||||
/**
|
||||
* Progressive Light Map Accumulator, by [zalo](https://github.com/zalo/).
|
||||
*
|
||||
* To use, simply construct a `ProgressiveLightMap` object,
|
||||
* `plmap.addObjectsToLightMap(object)` an array of semi-static
|
||||
* objects and lights to the class once, and then call
|
||||
* `plmap.update(camera)` every frame to begin accumulating
|
||||
* lighting samples.
|
||||
*
|
||||
* This should begin accumulating lightmaps which apply to
|
||||
* your objects, so you can start jittering lighting to achieve
|
||||
* the texture-space effect you're looking for.
|
||||
*
|
||||
* This class can only be used with {@link WebGLRenderer}.
|
||||
* When using {@link WebGPURenderer}, import from `ProgressiveLightMapGPU.js`.
|
||||
*
|
||||
* @three_import import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMap.js';
|
||||
*/
|
||||
class ProgressiveLightMap {
|
||||
|
||||
/**
|
||||
* Constructs a new progressive light map.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {number} [res=1024] - The side-long dimension of the total lightmap.
|
||||
*/
|
||||
constructor( renderer, res = 1024 ) {
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @type {WebGLRenderer}
|
||||
*/
|
||||
this.renderer = renderer;
|
||||
|
||||
/**
|
||||
* The side-long dimension of the total lightmap.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1024
|
||||
*/
|
||||
this.res = res;
|
||||
|
||||
// internals
|
||||
|
||||
this.lightMapContainers = [];
|
||||
this.scene = new Scene();
|
||||
this.buffer1Active = false;
|
||||
this.firstUpdate = true;
|
||||
this.labelMesh = null;
|
||||
this.blurringPlane = null;
|
||||
|
||||
// Create the Progressive LightMap Texture
|
||||
const format = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? HalfFloatType : FloatType;
|
||||
this.progressiveLightMap1 = new WebGLRenderTarget( this.res, this.res, { type: format } );
|
||||
this.progressiveLightMap2 = new WebGLRenderTarget( this.res, this.res, { type: format } );
|
||||
this.progressiveLightMap2.texture.channel = 1;
|
||||
|
||||
// Inject some spicy new logic into a standard phong material
|
||||
this.uvMat = new MeshPhongMaterial();
|
||||
this.uvMat.uniforms = {};
|
||||
this.uvMat.onBeforeCompile = ( shader ) => {
|
||||
|
||||
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
|
||||
shader.vertexShader =
|
||||
'attribute vec2 uv1;\n' +
|
||||
'#define USE_LIGHTMAP\n' +
|
||||
'#define LIGHTMAP_UV uv1\n' +
|
||||
shader.vertexShader.slice( 0, - 1 ) +
|
||||
' gl_Position = vec4((LIGHTMAP_UV - 0.5) * 2.0, 1.0, 1.0); }';
|
||||
|
||||
// Fragment Shader: Set Pixels to average in the Previous frame's Shadows
|
||||
const bodyStart = shader.fragmentShader.indexOf( 'void main() {' );
|
||||
shader.fragmentShader =
|
||||
'#define USE_LIGHTMAP\n' +
|
||||
shader.fragmentShader.slice( 0, bodyStart ) +
|
||||
' uniform sampler2D previousShadowMap;\n uniform float averagingWindow;\n' +
|
||||
shader.fragmentShader.slice( bodyStart - 1, - 1 ) +
|
||||
`\nvec3 texelOld = texture2D(previousShadowMap, vLightMapUv).rgb;
|
||||
gl_FragColor.rgb = mix(texelOld, gl_FragColor.rgb, 1.0/averagingWindow);
|
||||
}`;
|
||||
|
||||
// Set the Previous Frame's Texture Buffer and Averaging Window
|
||||
shader.uniforms.previousShadowMap = { value: this.progressiveLightMap1.texture };
|
||||
shader.uniforms.averagingWindow = { value: 100 };
|
||||
|
||||
this.uvMat.uniforms = shader.uniforms;
|
||||
|
||||
// Set the new Shader to this
|
||||
this.uvMat.userData.shader = shader;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets these objects' materials' lightmaps and modifies their uv1's.
|
||||
*
|
||||
* @param {Array<Object3D>} objects - An array of objects and lights to set up your lightmap.
|
||||
*/
|
||||
addObjectsToLightMap( objects ) {
|
||||
|
||||
// Prepare list of UV bounding boxes for packing later...
|
||||
this.uv_boxes = []; const padding = 3 / this.res;
|
||||
|
||||
for ( let ob = 0; ob < objects.length; ob ++ ) {
|
||||
|
||||
const object = objects[ ob ];
|
||||
|
||||
// If this object is a light, simply add it to the internal scene
|
||||
if ( object.isLight ) {
|
||||
|
||||
this.scene.attach( object ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( object.geometry.hasAttribute( 'uv' ) === false ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need uvs.' ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( object.geometry.hasAttribute( 'normal' ) === false ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need normals.' ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( this.blurringPlane === null ) {
|
||||
|
||||
this._initializeBlurPlane( this.res, this.progressiveLightMap1 );
|
||||
|
||||
}
|
||||
|
||||
// Apply the lightmap to the object
|
||||
object.material.lightMap = this.progressiveLightMap2.texture;
|
||||
object.material.dithering = true;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
object.renderOrder = 1000 + ob;
|
||||
|
||||
// Prepare UV boxes for potpack
|
||||
// TODO: Size these by object surface area
|
||||
this.uv_boxes.push( { w: 1 + ( padding * 2 ),
|
||||
h: 1 + ( padding * 2 ), index: ob } );
|
||||
|
||||
this.lightMapContainers.push( { basicMat: object.material, object: object } );
|
||||
|
||||
}
|
||||
|
||||
// Pack the objects' lightmap UVs into the same global space
|
||||
const dimensions = potpack( this.uv_boxes );
|
||||
this.uv_boxes.forEach( ( box ) => {
|
||||
|
||||
const uv1 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
|
||||
for ( let i = 0; i < uv1.array.length; i += uv1.itemSize ) {
|
||||
|
||||
uv1.array[ i ] = ( uv1.array[ i ] + box.x + padding ) / dimensions.w;
|
||||
uv1.array[ i + 1 ] = ( uv1.array[ i + 1 ] + box.y + padding ) / dimensions.h;
|
||||
|
||||
}
|
||||
|
||||
objects[ box.index ].geometry.setAttribute( 'uv1', uv1 );
|
||||
objects[ box.index ].geometry.getAttribute( 'uv1' ).needsUpdate = true;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This function renders each mesh one at a time into their respective surface maps.
|
||||
*
|
||||
* @param {Camera} camera - The camera the scene is rendered with.
|
||||
* @param {number} [blendWindow=100] - When >1, samples will accumulate over time.
|
||||
* @param {boolean} [blurEdges=true] - Whether to fix UV Edges via blurring.
|
||||
*/
|
||||
update( camera, blendWindow = 100, blurEdges = true ) {
|
||||
|
||||
if ( this.blurringPlane === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Store the original Render Target
|
||||
const oldTarget = this.renderer.getRenderTarget();
|
||||
|
||||
// The blurring plane applies blur to the seams of the lightmap
|
||||
this.blurringPlane.visible = blurEdges;
|
||||
|
||||
// Steal the Object3D from the real world to our special dimension
|
||||
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
|
||||
|
||||
this.lightMapContainers[ l ].object.oldScene =
|
||||
this.lightMapContainers[ l ].object.parent;
|
||||
this.scene.attach( this.lightMapContainers[ l ].object );
|
||||
|
||||
}
|
||||
|
||||
// Initialize everything
|
||||
if ( this.firstUpdate === true ) {
|
||||
|
||||
this.renderer.compile( this.scene, camera );
|
||||
this.firstUpdate = false;
|
||||
|
||||
}
|
||||
|
||||
// Set each object's material to the UV Unwrapped Surface Mapping Version
|
||||
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
|
||||
|
||||
this.uvMat.uniforms.averagingWindow = { value: blendWindow };
|
||||
this.lightMapContainers[ l ].object.material = this.uvMat;
|
||||
this.lightMapContainers[ l ].object.oldFrustumCulled =
|
||||
this.lightMapContainers[ l ].object.frustumCulled;
|
||||
this.lightMapContainers[ l ].object.frustumCulled = false;
|
||||
|
||||
}
|
||||
|
||||
// Ping-pong two surface buffers for reading/writing
|
||||
const activeMap = this.buffer1Active ? this.progressiveLightMap1 : this.progressiveLightMap2;
|
||||
const inactiveMap = this.buffer1Active ? this.progressiveLightMap2 : this.progressiveLightMap1;
|
||||
|
||||
// Render the object's surface maps
|
||||
this.renderer.setRenderTarget( activeMap );
|
||||
this.uvMat.uniforms.previousShadowMap = { value: inactiveMap.texture };
|
||||
this.blurringPlane.material.uniforms.previousShadowMap = { value: inactiveMap.texture };
|
||||
this.buffer1Active = ! this.buffer1Active;
|
||||
this.renderer.render( this.scene, camera );
|
||||
|
||||
// Restore the object's Real-time Material and add it back to the original world
|
||||
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
|
||||
|
||||
this.lightMapContainers[ l ].object.frustumCulled =
|
||||
this.lightMapContainers[ l ].object.oldFrustumCulled;
|
||||
this.lightMapContainers[ l ].object.material = this.lightMapContainers[ l ].basicMat;
|
||||
this.lightMapContainers[ l ].object.oldScene.attach( this.lightMapContainers[ l ].object );
|
||||
|
||||
}
|
||||
|
||||
// Restore the original Render Target
|
||||
this.renderer.setRenderTarget( oldTarget );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the lightmap in the main scene. Call this after adding the objects to it.
|
||||
*
|
||||
* @param {boolean} visible - Whether the debug plane should be visible
|
||||
* @param {Vector3} [position] - Where the debug plane should be drawn
|
||||
*/
|
||||
showDebugLightmap( visible, position = undefined ) {
|
||||
|
||||
if ( this.lightMapContainers.length === 0 ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: Call .showDebugLightmap() after adding the objects.' );
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if ( this.labelMesh === null ) {
|
||||
|
||||
const labelMaterial = new MeshBasicMaterial( { map: this.progressiveLightMap1.texture, side: DoubleSide } );
|
||||
const labelGeometry = new PlaneGeometry( 100, 100 );
|
||||
this.labelMesh = new Mesh( labelGeometry, labelMaterial );
|
||||
this.labelMesh.position.y = 250;
|
||||
this.lightMapContainers[ 0 ].object.parent.add( this.labelMesh );
|
||||
|
||||
}
|
||||
|
||||
if ( position !== undefined ) {
|
||||
|
||||
this.labelMesh.position.copy( position );
|
||||
|
||||
}
|
||||
|
||||
this.labelMesh.visible = visible;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Blurring Plane.
|
||||
*
|
||||
* @private
|
||||
* @param {number} res - The square resolution of this object's lightMap.
|
||||
* @param {WebGLRenderTarget} lightMap - The lightmap to initialize the plane with.
|
||||
*/
|
||||
_initializeBlurPlane( res, lightMap ) {
|
||||
|
||||
const blurMaterial = new MeshBasicMaterial();
|
||||
blurMaterial.uniforms = { previousShadowMap: { value: null },
|
||||
pixelOffset: { value: 1.0 / res },
|
||||
polygonOffset: true, polygonOffsetFactor: - 1, polygonOffsetUnits: 3.0 };
|
||||
blurMaterial.onBeforeCompile = ( shader ) => {
|
||||
|
||||
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
|
||||
shader.vertexShader =
|
||||
'#define USE_UV\n' +
|
||||
shader.vertexShader.slice( 0, - 1 ) +
|
||||
' gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }';
|
||||
|
||||
// Fragment Shader: Set Pixels to 9-tap box blur the current frame's Shadows
|
||||
const bodyStart = shader.fragmentShader.indexOf( 'void main() {' );
|
||||
shader.fragmentShader =
|
||||
'#define USE_UV\n' +
|
||||
shader.fragmentShader.slice( 0, bodyStart ) +
|
||||
' uniform sampler2D previousShadowMap;\n uniform float pixelOffset;\n' +
|
||||
shader.fragmentShader.slice( bodyStart - 1, - 1 ) +
|
||||
` gl_FragColor.rgb = (
|
||||
texture2D(previousShadowMap, vUv + vec2( pixelOffset, 0.0 )).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2( 0.0 , pixelOffset)).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2( 0.0 , -pixelOffset)).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, 0.0 )).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2( pixelOffset, pixelOffset)).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, pixelOffset)).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2( pixelOffset, -pixelOffset)).rgb +
|
||||
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, -pixelOffset)).rgb)/8.0;
|
||||
}`;
|
||||
|
||||
// Set the LightMap Accumulation Buffer
|
||||
shader.uniforms.previousShadowMap = { value: lightMap.texture };
|
||||
shader.uniforms.pixelOffset = { value: 0.5 / res };
|
||||
blurMaterial.uniforms = shader.uniforms;
|
||||
|
||||
// Set the new Shader to this
|
||||
blurMaterial.userData.shader = shader;
|
||||
|
||||
};
|
||||
|
||||
this.blurringPlane = new Mesh( new PlaneGeometry( 1, 1 ), blurMaterial );
|
||||
this.blurringPlane.name = 'Blurring Plane';
|
||||
this.blurringPlane.frustumCulled = false;
|
||||
this.blurringPlane.renderOrder = 0;
|
||||
this.blurringPlane.material.depthWrite = false;
|
||||
this.scene.add( this.blurringPlane );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees all internal resources.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this.progressiveLightMap1.dispose();
|
||||
this.progressiveLightMap2.dispose();
|
||||
|
||||
this.uvMat.dispose();
|
||||
|
||||
if ( this.blurringPlane !== null ) {
|
||||
|
||||
this.blurringPlane.geometry.dispose();
|
||||
this.blurringPlane.material.dispose();
|
||||
|
||||
}
|
||||
|
||||
if ( this.labelMesh !== null ) {
|
||||
|
||||
this.labelMesh.geometry.dispose();
|
||||
this.labelMesh.material.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ProgressiveLightMap };
|
||||
322
node_modules/three/examples/jsm/misc/ProgressiveLightMapGPU.js
generated
vendored
Normal file
322
node_modules/three/examples/jsm/misc/ProgressiveLightMapGPU.js
generated
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
import { DoubleSide, FloatType, HalfFloatType, PlaneGeometry, Mesh, RenderTarget, Scene, MeshPhongNodeMaterial, NodeMaterial } from 'three/webgpu';
|
||||
import { add, float, mix, output, sub, texture, uniform, uv, vec2, vec4 } from 'three/tsl';
|
||||
|
||||
import { potpack } from '../libs/potpack.module.js';
|
||||
|
||||
/**
|
||||
* Progressive Light Map Accumulator, by [zalo](https://github.com/zalo/).
|
||||
*
|
||||
* To use, simply construct a `ProgressiveLightMap` object,
|
||||
* `plmap.addObjectsToLightMap(object)` an array of semi-static
|
||||
* objects and lights to the class once, and then call
|
||||
* `plmap.update(camera)` every frame to begin accumulating
|
||||
* lighting samples.
|
||||
*
|
||||
* This should begin accumulating lightmaps which apply to
|
||||
* your objects, so you can start jittering lighting to achieve
|
||||
* the texture-space effect you're looking for.
|
||||
*
|
||||
* This class can only be used with {@link WebGPURenderer}.
|
||||
* When using {@link WebGLRenderer}, import from `ProgressiveLightMap.js`.
|
||||
*
|
||||
* @three_import import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMapGPU.js';
|
||||
*/
|
||||
class ProgressiveLightMap {
|
||||
|
||||
/**
|
||||
* @param {WebGPURenderer} renderer - The renderer.
|
||||
* @param {number} [resolution=1024] - The side-long dimension of the total lightmap.
|
||||
*/
|
||||
constructor( renderer, resolution = 1024 ) {
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @type {WebGPURenderer}
|
||||
*/
|
||||
this.renderer = renderer;
|
||||
|
||||
/**
|
||||
* The side-long dimension of the total lightmap.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1024
|
||||
*/
|
||||
this.resolution = resolution;
|
||||
|
||||
this._lightMapContainers = [];
|
||||
this._scene = new Scene();
|
||||
this._buffer1Active = false;
|
||||
this._labelMesh = null;
|
||||
this._blurringPlane = null;
|
||||
|
||||
// Create the Progressive LightMap Texture
|
||||
|
||||
const type = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? HalfFloatType : FloatType;
|
||||
this._progressiveLightMap1 = new RenderTarget( this.resolution, this.resolution, { type: type } );
|
||||
this._progressiveLightMap2 = new RenderTarget( this.resolution, this.resolution, { type: type } );
|
||||
this._progressiveLightMap2.texture.channel = 1;
|
||||
|
||||
// uniforms
|
||||
|
||||
this._averagingWindow = uniform( 100 );
|
||||
this._previousShadowMap = texture( this._progressiveLightMap1.texture );
|
||||
|
||||
// materials
|
||||
|
||||
const uvNode = uv( 1 ).flipY();
|
||||
|
||||
this._uvMat = new MeshPhongNodeMaterial();
|
||||
this._uvMat.vertexNode = vec4( sub( uvNode, vec2( 0.5 ) ).mul( 2 ), 1, 1 );
|
||||
this._uvMat.outputNode = vec4( mix( this._previousShadowMap.sample( uv( 1 ) ), output, float( 1 ).div( this._averagingWindow ) ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets these objects' materials' lightmaps and modifies their uv1's.
|
||||
*
|
||||
* @param {Array<Object3D>} objects - An array of objects and lights to set up your lightmap.
|
||||
*/
|
||||
addObjectsToLightMap( objects ) {
|
||||
|
||||
// Prepare list of UV bounding boxes for packing later...
|
||||
const uv_boxes = [];
|
||||
|
||||
const padding = 3 / this.resolution;
|
||||
|
||||
for ( let ob = 0; ob < objects.length; ob ++ ) {
|
||||
|
||||
const object = objects[ ob ];
|
||||
|
||||
// If this object is a light, simply add it to the internal scene
|
||||
if ( object.isLight ) {
|
||||
|
||||
this._scene.attach( object ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( object.geometry.hasAttribute( 'uv' ) === false ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need uvs.' ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( object.geometry.hasAttribute( 'normal' ) === false ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need normals.' ); continue;
|
||||
|
||||
}
|
||||
|
||||
if ( this._blurringPlane === null ) {
|
||||
|
||||
this._initializeBlurPlane();
|
||||
|
||||
}
|
||||
|
||||
// Apply the lightmap to the object
|
||||
object.material.lightMap = this._progressiveLightMap2.texture;
|
||||
object.material.dithering = true;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
object.renderOrder = 1000 + ob;
|
||||
|
||||
// Prepare UV boxes for potpack (potpack will update x and y)
|
||||
// TODO: Size these by object surface area
|
||||
uv_boxes.push( { w: 1 + ( padding * 2 ), h: 1 + ( padding * 2 ), index: ob, x: 0, y: 0 } );
|
||||
|
||||
this._lightMapContainers.push( { basicMat: object.material, object: object } );
|
||||
|
||||
}
|
||||
|
||||
// Pack the objects' lightmap UVs into the same global space
|
||||
const dimensions = potpack( uv_boxes );
|
||||
uv_boxes.forEach( ( box ) => {
|
||||
|
||||
const uv1 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
|
||||
for ( let i = 0; i < uv1.array.length; i += uv1.itemSize ) {
|
||||
|
||||
uv1.array[ i ] = ( uv1.array[ i ] + box.x + padding ) / dimensions.w;
|
||||
uv1.array[ i + 1 ] = 1 - ( ( uv1.array[ i + 1 ] + box.y + padding ) / dimensions.h );
|
||||
|
||||
}
|
||||
|
||||
objects[ box.index ].geometry.setAttribute( 'uv1', uv1 );
|
||||
objects[ box.index ].geometry.getAttribute( 'uv1' ).needsUpdate = true;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees all internal resources.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this._progressiveLightMap1.dispose();
|
||||
this._progressiveLightMap2.dispose();
|
||||
|
||||
this._uvMat.dispose();
|
||||
|
||||
if ( this._blurringPlane !== null ) {
|
||||
|
||||
this._blurringPlane.geometry.dispose();
|
||||
this._blurringPlane.material.dispose();
|
||||
|
||||
}
|
||||
|
||||
if ( this._labelMesh !== null ) {
|
||||
|
||||
this._labelMesh.geometry.dispose();
|
||||
this._labelMesh.material.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This function renders each mesh one at a time into their respective surface maps.
|
||||
*
|
||||
* @param {Camera} camera - The camera the scene is rendered with.
|
||||
* @param {number} [blendWindow=100] - When >1, samples will accumulate over time.
|
||||
* @param {boolean} [blurEdges=true] - Whether to fix UV Edges via blurring.
|
||||
*/
|
||||
update( camera, blendWindow = 100, blurEdges = true ) {
|
||||
|
||||
if ( this._blurringPlane === null ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Store the original Render Target
|
||||
const currentRenderTarget = this.renderer.getRenderTarget();
|
||||
|
||||
// The blurring plane applies blur to the seams of the lightmap
|
||||
this._blurringPlane.visible = blurEdges;
|
||||
|
||||
// Steal the Object3D from the real world to our special dimension
|
||||
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
|
||||
|
||||
this._lightMapContainers[ l ].object.oldScene = this._lightMapContainers[ l ].object.parent;
|
||||
this._scene.attach( this._lightMapContainers[ l ].object );
|
||||
|
||||
}
|
||||
|
||||
// Set each object's material to the UV Unwrapped Surface Mapping Version
|
||||
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
|
||||
|
||||
this._averagingWindow.value = blendWindow;
|
||||
this._lightMapContainers[ l ].object.material = this._uvMat;
|
||||
this._lightMapContainers[ l ].object.oldFrustumCulled = this._lightMapContainers[ l ].object.frustumCulled;
|
||||
this._lightMapContainers[ l ].object.frustumCulled = false;
|
||||
|
||||
}
|
||||
|
||||
// Ping-pong two surface buffers for reading/writing
|
||||
const activeMap = this._buffer1Active ? this._progressiveLightMap1 : this._progressiveLightMap2;
|
||||
const inactiveMap = this._buffer1Active ? this._progressiveLightMap2 : this._progressiveLightMap1;
|
||||
|
||||
// Render the object's surface maps
|
||||
this.renderer.setRenderTarget( activeMap );
|
||||
this._previousShadowMap.value = inactiveMap.texture;
|
||||
|
||||
this._buffer1Active = ! this._buffer1Active;
|
||||
this.renderer.render( this._scene, camera );
|
||||
|
||||
// Restore the object's Real-time Material and add it back to the original world
|
||||
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
|
||||
|
||||
this._lightMapContainers[ l ].object.frustumCulled = this._lightMapContainers[ l ].object.oldFrustumCulled;
|
||||
this._lightMapContainers[ l ].object.material = this._lightMapContainers[ l ].basicMat;
|
||||
this._lightMapContainers[ l ].object.oldScene.attach( this._lightMapContainers[ l ].object );
|
||||
|
||||
}
|
||||
|
||||
// Restore the original Render Target
|
||||
this.renderer.setRenderTarget( currentRenderTarget );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the lightmap in the main scene. Call this after adding the objects to it.
|
||||
*
|
||||
* @param {boolean} visible - Whether the debug plane should be visible
|
||||
* @param {Vector3} [position] - Where the debug plane should be drawn
|
||||
*/
|
||||
showDebugLightmap( visible, position = null ) {
|
||||
|
||||
if ( this._lightMapContainers.length === 0 ) {
|
||||
|
||||
console.warn( 'THREE.ProgressiveLightMap: Call .showDebugLightmap() after adding the objects.' );
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if ( this._labelMesh === null ) {
|
||||
|
||||
const labelMaterial = new NodeMaterial();
|
||||
labelMaterial.colorNode = texture( this._progressiveLightMap1.texture ).sample( uv().flipY() );
|
||||
labelMaterial.side = DoubleSide;
|
||||
|
||||
const labelGeometry = new PlaneGeometry( 100, 100 );
|
||||
|
||||
this._labelMesh = new Mesh( labelGeometry, labelMaterial );
|
||||
this._labelMesh.position.y = 250;
|
||||
|
||||
this._lightMapContainers[ 0 ].object.parent.add( this._labelMesh );
|
||||
|
||||
}
|
||||
|
||||
if ( position !== null ) {
|
||||
|
||||
this._labelMesh.position.copy( position );
|
||||
|
||||
}
|
||||
|
||||
this._labelMesh.visible = visible;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Blurring Plane.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initializeBlurPlane() {
|
||||
|
||||
const blurMaterial = new NodeMaterial();
|
||||
blurMaterial.polygonOffset = true;
|
||||
blurMaterial.polygonOffsetFactor = - 1;
|
||||
blurMaterial.polygonOffsetUnits = 3;
|
||||
|
||||
blurMaterial.vertexNode = vec4( sub( uv(), vec2( 0.5 ) ).mul( 2 ), 1, 1 );
|
||||
|
||||
const uvNode = uv().flipY().toVar();
|
||||
const pixelOffset = float( 0.5 ).div( float( this.resolution ) ).toVar();
|
||||
|
||||
const color = add(
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, 0 ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( 0, pixelOffset ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( 0, pixelOffset.negate() ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), 0 ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, pixelOffset ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), pixelOffset ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, pixelOffset.negate() ) ) ),
|
||||
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), pixelOffset.negate() ) ) ),
|
||||
).div( 8 );
|
||||
|
||||
blurMaterial.fragmentNode = color;
|
||||
|
||||
this._blurringPlane = new Mesh( new PlaneGeometry( 1, 1 ), blurMaterial );
|
||||
this._blurringPlane.name = 'Blurring Plane';
|
||||
this._blurringPlane.frustumCulled = false;
|
||||
this._blurringPlane.renderOrder = 0;
|
||||
this._blurringPlane.material.depthWrite = false;
|
||||
this._scene.add( this._blurringPlane );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ProgressiveLightMap };
|
||||
623
node_modules/three/examples/jsm/misc/RollerCoaster.js
generated
vendored
Normal file
623
node_modules/three/examples/jsm/misc/RollerCoaster.js
generated
vendored
Normal file
@@ -0,0 +1,623 @@
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
SRGBColorSpace,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* A procedural roller coaster geometry.
|
||||
*
|
||||
* @augments BufferGeometry
|
||||
* @three_import import { RollerCoasterGeometry } from 'three/addons/misc/RollerCoaster.js';
|
||||
*/
|
||||
class RollerCoasterGeometry extends BufferGeometry {
|
||||
|
||||
/**
|
||||
* Constructs a new geometry.
|
||||
*
|
||||
* @param {Curve} curve - The curve to generate the geometry along.
|
||||
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
|
||||
*/
|
||||
constructor( curve, divisions ) {
|
||||
|
||||
super();
|
||||
|
||||
const vertices = [];
|
||||
const normals = [];
|
||||
const colors = [];
|
||||
|
||||
const color1 = [ 1, 1, 1 ];
|
||||
const color2 = [ 1, 1, 0 ];
|
||||
|
||||
const up = new Vector3( 0, 1, 0 );
|
||||
const forward = new Vector3();
|
||||
const right = new Vector3();
|
||||
|
||||
const quaternion = new Quaternion();
|
||||
const prevQuaternion = new Quaternion();
|
||||
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
|
||||
|
||||
const point = new Vector3();
|
||||
const prevPoint = new Vector3();
|
||||
prevPoint.copy( curve.getPointAt( 0 ) );
|
||||
|
||||
// shapes
|
||||
|
||||
const step = [
|
||||
new Vector3( - 0.225, 0, 0 ),
|
||||
new Vector3( 0, - 0.050, 0 ),
|
||||
new Vector3( 0, - 0.175, 0 ),
|
||||
|
||||
new Vector3( 0, - 0.050, 0 ),
|
||||
new Vector3( 0.225, 0, 0 ),
|
||||
new Vector3( 0, - 0.175, 0 )
|
||||
];
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
|
||||
let sides = 5;
|
||||
const tube1 = [];
|
||||
|
||||
for ( let i = 0; i < sides; i ++ ) {
|
||||
|
||||
const angle = ( i / sides ) * PI2;
|
||||
tube1.push( new Vector3( Math.sin( angle ) * 0.06, Math.cos( angle ) * 0.06, 0 ) );
|
||||
|
||||
}
|
||||
|
||||
sides = 6;
|
||||
const tube2 = [];
|
||||
|
||||
for ( let i = 0; i < sides; i ++ ) {
|
||||
|
||||
const angle = ( i / sides ) * PI2;
|
||||
tube2.push( new Vector3( Math.sin( angle ) * 0.025, Math.cos( angle ) * 0.025, 0 ) );
|
||||
|
||||
}
|
||||
|
||||
const vector = new Vector3();
|
||||
const normal = new Vector3();
|
||||
|
||||
function drawShape( shape, color ) {
|
||||
|
||||
normal.set( 0, 0, - 1 ).applyQuaternion( quaternion );
|
||||
|
||||
for ( let j = 0; j < shape.length; j ++ ) {
|
||||
|
||||
vector.copy( shape[ j ] );
|
||||
vector.applyQuaternion( quaternion );
|
||||
vector.add( point );
|
||||
|
||||
vertices.push( vector.x, vector.y, vector.z );
|
||||
normals.push( normal.x, normal.y, normal.z );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
|
||||
}
|
||||
|
||||
normal.set( 0, 0, 1 ).applyQuaternion( quaternion );
|
||||
|
||||
for ( let j = shape.length - 1; j >= 0; j -- ) {
|
||||
|
||||
vector.copy( shape[ j ] );
|
||||
vector.applyQuaternion( quaternion );
|
||||
vector.add( point );
|
||||
|
||||
vertices.push( vector.x, vector.y, vector.z );
|
||||
normals.push( normal.x, normal.y, normal.z );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const vector1 = new Vector3();
|
||||
const vector2 = new Vector3();
|
||||
const vector3 = new Vector3();
|
||||
const vector4 = new Vector3();
|
||||
|
||||
const normal1 = new Vector3();
|
||||
const normal2 = new Vector3();
|
||||
const normal3 = new Vector3();
|
||||
const normal4 = new Vector3();
|
||||
|
||||
function extrudeShape( shape, offset, color ) {
|
||||
|
||||
for ( let j = 0, jl = shape.length; j < jl; j ++ ) {
|
||||
|
||||
const point1 = shape[ j ];
|
||||
const point2 = shape[ ( j + 1 ) % jl ];
|
||||
|
||||
vector1.copy( point1 ).add( offset );
|
||||
vector1.applyQuaternion( quaternion );
|
||||
vector1.add( point );
|
||||
|
||||
vector2.copy( point2 ).add( offset );
|
||||
vector2.applyQuaternion( quaternion );
|
||||
vector2.add( point );
|
||||
|
||||
vector3.copy( point2 ).add( offset );
|
||||
vector3.applyQuaternion( prevQuaternion );
|
||||
vector3.add( prevPoint );
|
||||
|
||||
vector4.copy( point1 ).add( offset );
|
||||
vector4.applyQuaternion( prevQuaternion );
|
||||
vector4.add( prevPoint );
|
||||
|
||||
vertices.push( vector1.x, vector1.y, vector1.z );
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector3.x, vector3.y, vector3.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
//
|
||||
|
||||
normal1.copy( point1 );
|
||||
normal1.applyQuaternion( quaternion );
|
||||
normal1.normalize();
|
||||
|
||||
normal2.copy( point2 );
|
||||
normal2.applyQuaternion( quaternion );
|
||||
normal2.normalize();
|
||||
|
||||
normal3.copy( point2 );
|
||||
normal3.applyQuaternion( prevQuaternion );
|
||||
normal3.normalize();
|
||||
|
||||
normal4.copy( point1 );
|
||||
normal4.applyQuaternion( prevQuaternion );
|
||||
normal4.normalize();
|
||||
|
||||
normals.push( normal1.x, normal1.y, normal1.z );
|
||||
normals.push( normal2.x, normal2.y, normal2.z );
|
||||
normals.push( normal4.x, normal4.y, normal4.z );
|
||||
|
||||
normals.push( normal2.x, normal2.y, normal2.z );
|
||||
normals.push( normal3.x, normal3.y, normal3.z );
|
||||
normals.push( normal4.x, normal4.y, normal4.z );
|
||||
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const offset = new Vector3();
|
||||
|
||||
for ( let i = 1; i <= divisions; i ++ ) {
|
||||
|
||||
point.copy( curve.getPointAt( i / divisions ) );
|
||||
|
||||
up.set( 0, 1, 0 );
|
||||
|
||||
forward.subVectors( point, prevPoint ).normalize();
|
||||
right.crossVectors( up, forward ).normalize();
|
||||
up.crossVectors( forward, right );
|
||||
|
||||
const angle = Math.atan2( forward.x, forward.z );
|
||||
|
||||
quaternion.setFromAxisAngle( up, angle );
|
||||
|
||||
if ( i % 2 === 0 ) {
|
||||
|
||||
drawShape( step, color2 );
|
||||
|
||||
}
|
||||
|
||||
extrudeShape( tube1, offset.set( 0, - 0.125, 0 ), color2 );
|
||||
extrudeShape( tube2, offset.set( 0.2, 0, 0 ), color1 );
|
||||
extrudeShape( tube2, offset.set( - 0.2, 0, 0 ), color1 );
|
||||
|
||||
prevPoint.copy( point );
|
||||
prevQuaternion.copy( quaternion );
|
||||
|
||||
}
|
||||
|
||||
// console.log( vertices.length );
|
||||
|
||||
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
|
||||
this.setAttribute( 'normal', new BufferAttribute( new Float32Array( normals ), 3 ) );
|
||||
this.setAttribute( 'color', new BufferAttribute( new Float32Array( colors ), 3 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A procedural roller coaster lifters geometry.
|
||||
*
|
||||
* @augments BufferGeometry
|
||||
* @three_import import { RollerCoasterLiftersGeometry } from 'three/addons/misc/RollerCoaster.js';
|
||||
*/
|
||||
class RollerCoasterLiftersGeometry extends BufferGeometry {
|
||||
|
||||
/**
|
||||
* Constructs a new geometry.
|
||||
*
|
||||
* @param {Curve} curve - The curve to generate the geometry along.
|
||||
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
|
||||
*/
|
||||
constructor( curve, divisions ) {
|
||||
|
||||
super();
|
||||
|
||||
const vertices = [];
|
||||
const normals = [];
|
||||
|
||||
const quaternion = new Quaternion();
|
||||
|
||||
const up = new Vector3( 0, 1, 0 );
|
||||
|
||||
const point = new Vector3();
|
||||
const tangent = new Vector3();
|
||||
|
||||
// shapes
|
||||
|
||||
const tube1 = [
|
||||
new Vector3( 0, 0.05, - 0.05 ),
|
||||
new Vector3( 0, 0.05, 0.05 ),
|
||||
new Vector3( 0, - 0.05, 0 )
|
||||
];
|
||||
|
||||
const tube2 = [
|
||||
new Vector3( - 0.05, 0, 0.05 ),
|
||||
new Vector3( - 0.05, 0, - 0.05 ),
|
||||
new Vector3( 0.05, 0, 0 )
|
||||
];
|
||||
|
||||
const tube3 = [
|
||||
new Vector3( 0.05, 0, - 0.05 ),
|
||||
new Vector3( 0.05, 0, 0.05 ),
|
||||
new Vector3( - 0.05, 0, 0 )
|
||||
];
|
||||
|
||||
const vector1 = new Vector3();
|
||||
const vector2 = new Vector3();
|
||||
const vector3 = new Vector3();
|
||||
const vector4 = new Vector3();
|
||||
|
||||
const normal1 = new Vector3();
|
||||
const normal2 = new Vector3();
|
||||
const normal3 = new Vector3();
|
||||
const normal4 = new Vector3();
|
||||
|
||||
function extrudeShape( shape, fromPoint, toPoint ) {
|
||||
|
||||
for ( let j = 0, jl = shape.length; j < jl; j ++ ) {
|
||||
|
||||
const point1 = shape[ j ];
|
||||
const point2 = shape[ ( j + 1 ) % jl ];
|
||||
|
||||
vector1.copy( point1 );
|
||||
vector1.applyQuaternion( quaternion );
|
||||
vector1.add( fromPoint );
|
||||
|
||||
vector2.copy( point2 );
|
||||
vector2.applyQuaternion( quaternion );
|
||||
vector2.add( fromPoint );
|
||||
|
||||
vector3.copy( point2 );
|
||||
vector3.applyQuaternion( quaternion );
|
||||
vector3.add( toPoint );
|
||||
|
||||
vector4.copy( point1 );
|
||||
vector4.applyQuaternion( quaternion );
|
||||
vector4.add( toPoint );
|
||||
|
||||
vertices.push( vector1.x, vector1.y, vector1.z );
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector3.x, vector3.y, vector3.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
//
|
||||
|
||||
normal1.copy( point1 );
|
||||
normal1.applyQuaternion( quaternion );
|
||||
normal1.normalize();
|
||||
|
||||
normal2.copy( point2 );
|
||||
normal2.applyQuaternion( quaternion );
|
||||
normal2.normalize();
|
||||
|
||||
normal3.copy( point2 );
|
||||
normal3.applyQuaternion( quaternion );
|
||||
normal3.normalize();
|
||||
|
||||
normal4.copy( point1 );
|
||||
normal4.applyQuaternion( quaternion );
|
||||
normal4.normalize();
|
||||
|
||||
normals.push( normal1.x, normal1.y, normal1.z );
|
||||
normals.push( normal2.x, normal2.y, normal2.z );
|
||||
normals.push( normal4.x, normal4.y, normal4.z );
|
||||
|
||||
normals.push( normal2.x, normal2.y, normal2.z );
|
||||
normals.push( normal3.x, normal3.y, normal3.z );
|
||||
normals.push( normal4.x, normal4.y, normal4.z );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const fromPoint = new Vector3();
|
||||
const toPoint = new Vector3();
|
||||
|
||||
for ( let i = 1; i <= divisions; i ++ ) {
|
||||
|
||||
point.copy( curve.getPointAt( i / divisions ) );
|
||||
tangent.copy( curve.getTangentAt( i / divisions ) );
|
||||
|
||||
const angle = Math.atan2( tangent.x, tangent.z );
|
||||
|
||||
quaternion.setFromAxisAngle( up, angle );
|
||||
|
||||
//
|
||||
|
||||
if ( point.y > 10 ) {
|
||||
|
||||
fromPoint.set( - 0.75, - 0.35, 0 );
|
||||
fromPoint.applyQuaternion( quaternion );
|
||||
fromPoint.add( point );
|
||||
|
||||
toPoint.set( 0.75, - 0.35, 0 );
|
||||
toPoint.applyQuaternion( quaternion );
|
||||
toPoint.add( point );
|
||||
|
||||
extrudeShape( tube1, fromPoint, toPoint );
|
||||
|
||||
fromPoint.set( - 0.7, - 0.3, 0 );
|
||||
fromPoint.applyQuaternion( quaternion );
|
||||
fromPoint.add( point );
|
||||
|
||||
toPoint.set( - 0.7, - point.y, 0 );
|
||||
toPoint.applyQuaternion( quaternion );
|
||||
toPoint.add( point );
|
||||
|
||||
extrudeShape( tube2, fromPoint, toPoint );
|
||||
|
||||
fromPoint.set( 0.7, - 0.3, 0 );
|
||||
fromPoint.applyQuaternion( quaternion );
|
||||
fromPoint.add( point );
|
||||
|
||||
toPoint.set( 0.7, - point.y, 0 );
|
||||
toPoint.applyQuaternion( quaternion );
|
||||
toPoint.add( point );
|
||||
|
||||
extrudeShape( tube3, fromPoint, toPoint );
|
||||
|
||||
} else {
|
||||
|
||||
fromPoint.set( 0, - 0.2, 0 );
|
||||
fromPoint.applyQuaternion( quaternion );
|
||||
fromPoint.add( point );
|
||||
|
||||
toPoint.set( 0, - point.y, 0 );
|
||||
toPoint.applyQuaternion( quaternion );
|
||||
toPoint.add( point );
|
||||
|
||||
extrudeShape( tube3, fromPoint, toPoint );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
|
||||
this.setAttribute( 'normal', new BufferAttribute( new Float32Array( normals ), 3 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A procedural roller coaster shadow geometry.
|
||||
*
|
||||
* @augments BufferGeometry
|
||||
* @three_import import { RollerCoasterShadowGeometry } from 'three/addons/misc/RollerCoaster.js';
|
||||
*/
|
||||
class RollerCoasterShadowGeometry extends BufferGeometry {
|
||||
|
||||
/**
|
||||
* Constructs a new geometry.
|
||||
*
|
||||
* @param {Curve} curve - The curve to generate the geometry along.
|
||||
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
|
||||
*/
|
||||
constructor( curve, divisions ) {
|
||||
|
||||
super();
|
||||
|
||||
const vertices = [];
|
||||
|
||||
const up = new Vector3( 0, 1, 0 );
|
||||
const forward = new Vector3();
|
||||
|
||||
const quaternion = new Quaternion();
|
||||
const prevQuaternion = new Quaternion();
|
||||
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
|
||||
|
||||
const point = new Vector3();
|
||||
|
||||
const prevPoint = new Vector3();
|
||||
prevPoint.copy( curve.getPointAt( 0 ) );
|
||||
prevPoint.y = 0;
|
||||
|
||||
const vector1 = new Vector3();
|
||||
const vector2 = new Vector3();
|
||||
const vector3 = new Vector3();
|
||||
const vector4 = new Vector3();
|
||||
|
||||
for ( let i = 1; i <= divisions; i ++ ) {
|
||||
|
||||
point.copy( curve.getPointAt( i / divisions ) );
|
||||
point.y = 0;
|
||||
|
||||
forward.subVectors( point, prevPoint );
|
||||
|
||||
const angle = Math.atan2( forward.x, forward.z );
|
||||
|
||||
quaternion.setFromAxisAngle( up, angle );
|
||||
|
||||
vector1.set( - 0.3, 0, 0 );
|
||||
vector1.applyQuaternion( quaternion );
|
||||
vector1.add( point );
|
||||
|
||||
vector2.set( 0.3, 0, 0 );
|
||||
vector2.applyQuaternion( quaternion );
|
||||
vector2.add( point );
|
||||
|
||||
vector3.set( 0.3, 0, 0 );
|
||||
vector3.applyQuaternion( prevQuaternion );
|
||||
vector3.add( prevPoint );
|
||||
|
||||
vector4.set( - 0.3, 0, 0 );
|
||||
vector4.applyQuaternion( prevQuaternion );
|
||||
vector4.add( prevPoint );
|
||||
|
||||
vertices.push( vector1.x, vector1.y, vector1.z );
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
vertices.push( vector2.x, vector2.y, vector2.z );
|
||||
vertices.push( vector3.x, vector3.y, vector3.z );
|
||||
vertices.push( vector4.x, vector4.y, vector4.z );
|
||||
|
||||
prevPoint.copy( point );
|
||||
prevQuaternion.copy( quaternion );
|
||||
|
||||
}
|
||||
|
||||
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A procedural sky geometry.
|
||||
*
|
||||
* @augments BufferGeometry
|
||||
* @three_import import { SkyGeometry } from 'three/addons/misc/RollerCoaster.js';
|
||||
*/
|
||||
class SkyGeometry extends BufferGeometry {
|
||||
|
||||
/**
|
||||
* Constructs a new geometry.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
const vertices = [];
|
||||
|
||||
for ( let i = 0; i < 100; i ++ ) {
|
||||
|
||||
const x = Math.random() * 800 - 400;
|
||||
const y = Math.random() * 50 + 50;
|
||||
const z = Math.random() * 800 - 400;
|
||||
|
||||
const size = Math.random() * 40 + 20;
|
||||
|
||||
vertices.push( x - size, y, z - size );
|
||||
vertices.push( x + size, y, z - size );
|
||||
vertices.push( x - size, y, z + size );
|
||||
|
||||
vertices.push( x + size, y, z - size );
|
||||
vertices.push( x + size, y, z + size );
|
||||
vertices.push( x - size, y, z + size );
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A procedural trees geometry.
|
||||
*
|
||||
* @augments BufferGeometry
|
||||
* @three_import import { TreesGeometry } from 'three/addons/misc/RollerCoaster.js';
|
||||
*/
|
||||
class TreesGeometry extends BufferGeometry {
|
||||
|
||||
/**
|
||||
* Constructs a new geometry.
|
||||
*
|
||||
* @param {Mesh} landscape - A mesh representing the landscape. Trees will be positioned
|
||||
* randomly on the landscape's surface.
|
||||
*/
|
||||
constructor( landscape ) {
|
||||
|
||||
super();
|
||||
|
||||
const vertices = [];
|
||||
const colors = [];
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
raycaster.ray.direction.set( 0, - 1, 0 );
|
||||
|
||||
const _color = new Color();
|
||||
|
||||
for ( let i = 0; i < 2000; i ++ ) {
|
||||
|
||||
const x = Math.random() * 500 - 250;
|
||||
const z = Math.random() * 500 - 250;
|
||||
|
||||
raycaster.ray.origin.set( x, 50, z );
|
||||
|
||||
const intersections = raycaster.intersectObject( landscape );
|
||||
|
||||
if ( intersections.length === 0 ) continue;
|
||||
|
||||
const y = intersections[ 0 ].point.y;
|
||||
|
||||
const height = Math.random() * 5 + 0.5;
|
||||
|
||||
let angle = Math.random() * Math.PI * 2;
|
||||
|
||||
vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
|
||||
vertices.push( x, y + height, z );
|
||||
vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
|
||||
|
||||
angle += Math.PI / 2;
|
||||
|
||||
vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
|
||||
vertices.push( x, y + height, z );
|
||||
vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
|
||||
|
||||
const random = Math.random() * 0.1;
|
||||
|
||||
for ( let j = 0; j < 6; j ++ ) {
|
||||
|
||||
_color.setRGB( 0.2 + random, 0.4 + random, 0, SRGBColorSpace );
|
||||
|
||||
colors.push( _color.r, _color.g, _color.b );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
|
||||
this.setAttribute( 'color', new BufferAttribute( new Float32Array( colors ), 3 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { RollerCoasterGeometry, RollerCoasterLiftersGeometry, RollerCoasterShadowGeometry, SkyGeometry, TreesGeometry };
|
||||
599
node_modules/three/examples/jsm/misc/TubePainter.js
generated
vendored
Normal file
599
node_modules/three/examples/jsm/misc/TubePainter.js
generated
vendored
Normal file
@@ -0,0 +1,599 @@
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DynamicDrawUsage,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* @classdesc This module can be used to paint tube-like meshes
|
||||
* along a sequence of points. This module is used in a XR
|
||||
* painter demo.
|
||||
*
|
||||
* ```js
|
||||
* const painter = new TubePainter();
|
||||
* scene.add( painter.mesh );
|
||||
* ```
|
||||
*
|
||||
* @name TubePainter
|
||||
* @class
|
||||
* @three_import import { TubePainter } from 'three/addons/misc/TubePainter.js';
|
||||
*/
|
||||
function TubePainter() {
|
||||
|
||||
const BUFFER_SIZE = 1000000 * 3;
|
||||
|
||||
const positions = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
|
||||
positions.usage = DynamicDrawUsage;
|
||||
|
||||
const normals = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
|
||||
normals.usage = DynamicDrawUsage;
|
||||
|
||||
const colors = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
|
||||
colors.usage = DynamicDrawUsage;
|
||||
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute( 'position', positions );
|
||||
geometry.setAttribute( 'normal', normals );
|
||||
geometry.setAttribute( 'color', colors );
|
||||
geometry.drawRange.count = 0;
|
||||
|
||||
const material = new MeshStandardMaterial( { vertexColors: true } );
|
||||
|
||||
const mesh = new Mesh( geometry, material );
|
||||
mesh.frustumCulled = false;
|
||||
|
||||
//
|
||||
|
||||
function getPoints( size ) {
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
|
||||
const sides = 15;
|
||||
const array = [];
|
||||
const radius = 0.01 * size;
|
||||
|
||||
for ( let i = 0; i < sides; i ++ ) {
|
||||
|
||||
const angle = ( i / sides ) * PI2;
|
||||
array.push( new Vector3( Math.sin( angle ) * radius, Math.cos( angle ) * radius, 0 ) );
|
||||
|
||||
}
|
||||
|
||||
return array;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const vector = new Vector3();
|
||||
|
||||
const vector1 = new Vector3();
|
||||
const vector2 = new Vector3();
|
||||
const vector3 = new Vector3();
|
||||
const vector4 = new Vector3();
|
||||
|
||||
const color1 = new Color( 0xffffff );
|
||||
const color2 = new Color( 0xffffff );
|
||||
|
||||
let size1 = 1;
|
||||
let size2 = 1;
|
||||
|
||||
function addCap( position, matrix, isEndCap, capSize ) {
|
||||
|
||||
let count = geometry.drawRange.count;
|
||||
|
||||
const points = getPoints( capSize );
|
||||
const sides = points.length;
|
||||
const radius = 0.01 * capSize;
|
||||
const latSegments = 4;
|
||||
const directionSign = isEndCap ? - 1 : 1;
|
||||
|
||||
for ( let lat = 0; lat < latSegments; lat ++ ) {
|
||||
|
||||
const phi1 = ( lat / latSegments ) * Math.PI * 0.5;
|
||||
const phi2 = ( ( lat + 1 ) / latSegments ) * Math.PI * 0.5;
|
||||
|
||||
const z1 = Math.sin( phi1 ) * radius * directionSign;
|
||||
const r1 = Math.cos( phi1 ) * radius;
|
||||
|
||||
const z2 = Math.sin( phi2 ) * radius * directionSign;
|
||||
const r2 = Math.cos( phi2 ) * radius;
|
||||
|
||||
for ( let i = 0; i < sides; i ++ ) {
|
||||
|
||||
const theta1 = ( i / sides ) * Math.PI * 2;
|
||||
const theta2 = ( ( i + 1 ) / sides ) * Math.PI * 2;
|
||||
|
||||
// First ring
|
||||
const x1 = Math.sin( theta1 ) * r1;
|
||||
const y1 = Math.cos( theta1 ) * r1;
|
||||
|
||||
const x2 = Math.sin( theta2 ) * r1;
|
||||
const y2 = Math.cos( theta2 ) * r1;
|
||||
|
||||
// Second ring
|
||||
const x3 = Math.sin( theta1 ) * r2;
|
||||
const y3 = Math.cos( theta1 ) * r2;
|
||||
|
||||
const x4 = Math.sin( theta2 ) * r2;
|
||||
const y4 = Math.cos( theta2 ) * r2;
|
||||
|
||||
// Transform to world space
|
||||
vector1.set( x1, y1, z1 ).applyMatrix4( matrix ).add( position );
|
||||
vector2.set( x2, y2, z1 ).applyMatrix4( matrix ).add( position );
|
||||
vector3.set( x3, y3, z2 ).applyMatrix4( matrix ).add( position );
|
||||
vector4.set( x4, y4, z2 ).applyMatrix4( matrix ).add( position );
|
||||
|
||||
// First triangle
|
||||
normal.set( x1, y1, z1 ).normalize().transformDirection( matrix );
|
||||
vector.set( x2, y2, z1 ).normalize().transformDirection( matrix );
|
||||
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
|
||||
|
||||
if ( isEndCap ) {
|
||||
|
||||
vector1.toArray( positions.array, count * 3 );
|
||||
vector2.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector3.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
normal.toArray( normals.array, count * 3 );
|
||||
vector.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
side.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
} else {
|
||||
|
||||
vector1.toArray( positions.array, count * 3 );
|
||||
vector3.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector2.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
normal.toArray( normals.array, count * 3 );
|
||||
side.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
vector.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
}
|
||||
|
||||
color1.toArray( colors.array, count * 3 );
|
||||
color1.toArray( colors.array, ( count + 1 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 2 ) * 3 );
|
||||
|
||||
count += 3;
|
||||
|
||||
// Second triangle
|
||||
if ( r2 > 0.001 ) {
|
||||
|
||||
normal.set( x2, y2, z1 ).normalize().transformDirection( matrix );
|
||||
vector.set( x4, y4, z2 ).normalize().transformDirection( matrix );
|
||||
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
|
||||
|
||||
if ( isEndCap ) {
|
||||
|
||||
vector2.toArray( positions.array, count * 3 );
|
||||
vector4.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector3.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
normal.toArray( normals.array, count * 3 );
|
||||
vector.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
side.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
} else {
|
||||
|
||||
vector3.toArray( positions.array, count * 3 );
|
||||
vector4.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector2.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
side.toArray( normals.array, count * 3 );
|
||||
vector.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
normal.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
}
|
||||
|
||||
color1.toArray( colors.array, count * 3 );
|
||||
color1.toArray( colors.array, ( count + 1 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 2 ) * 3 );
|
||||
|
||||
count += 3;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
geometry.drawRange.count = count;
|
||||
|
||||
}
|
||||
|
||||
function updateEndCap( position, matrix, capSize ) {
|
||||
|
||||
if ( endCapStartIndex === null ) return;
|
||||
|
||||
const points = getPoints( capSize );
|
||||
const sides = points.length;
|
||||
const radius = 0.01 * capSize;
|
||||
const latSegments = 4;
|
||||
|
||||
let count = endCapStartIndex;
|
||||
|
||||
for ( let lat = 0; lat < latSegments; lat ++ ) {
|
||||
|
||||
const phi1 = ( lat / latSegments ) * Math.PI * 0.5;
|
||||
const phi2 = ( ( lat + 1 ) / latSegments ) * Math.PI * 0.5;
|
||||
|
||||
const z1 = - Math.sin( phi1 ) * radius;
|
||||
const r1 = Math.cos( phi1 ) * radius;
|
||||
|
||||
const z2 = - Math.sin( phi2 ) * radius;
|
||||
const r2 = Math.cos( phi2 ) * radius;
|
||||
|
||||
for ( let i = 0; i < sides; i ++ ) {
|
||||
|
||||
const theta1 = ( i / sides ) * Math.PI * 2;
|
||||
const theta2 = ( ( i + 1 ) / sides ) * Math.PI * 2;
|
||||
|
||||
// First ring
|
||||
const x1 = Math.sin( theta1 ) * r1;
|
||||
const y1 = Math.cos( theta1 ) * r1;
|
||||
|
||||
const x2 = Math.sin( theta2 ) * r1;
|
||||
const y2 = Math.cos( theta2 ) * r1;
|
||||
|
||||
// Second ring
|
||||
const x3 = Math.sin( theta1 ) * r2;
|
||||
const y3 = Math.cos( theta1 ) * r2;
|
||||
|
||||
const x4 = Math.sin( theta2 ) * r2;
|
||||
const y4 = Math.cos( theta2 ) * r2;
|
||||
|
||||
// Transform positions to world space
|
||||
vector1.set( x1, y1, z1 ).applyMatrix4( matrix ).add( position );
|
||||
vector2.set( x2, y2, z1 ).applyMatrix4( matrix ).add( position );
|
||||
vector3.set( x3, y3, z2 ).applyMatrix4( matrix ).add( position );
|
||||
vector4.set( x4, y4, z2 ).applyMatrix4( matrix ).add( position );
|
||||
|
||||
// Transform normals to world space
|
||||
normal.set( x1, y1, z1 ).normalize().transformDirection( matrix );
|
||||
vector.set( x2, y2, z1 ).normalize().transformDirection( matrix );
|
||||
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
|
||||
|
||||
// First triangle
|
||||
vector1.toArray( positions.array, count * 3 );
|
||||
vector2.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector3.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
normal.toArray( normals.array, count * 3 );
|
||||
vector.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
side.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
color1.toArray( colors.array, count * 3 );
|
||||
color1.toArray( colors.array, ( count + 1 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 2 ) * 3 );
|
||||
|
||||
count += 3;
|
||||
|
||||
// Second triangle
|
||||
if ( r2 > 0.001 ) {
|
||||
|
||||
normal.set( x2, y2, z1 ).normalize().transformDirection( matrix );
|
||||
vector.set( x4, y4, z2 ).normalize().transformDirection( matrix );
|
||||
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
|
||||
|
||||
vector2.toArray( positions.array, count * 3 );
|
||||
vector4.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector3.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
normal.toArray( normals.array, count * 3 );
|
||||
vector.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
side.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
color1.toArray( colors.array, count * 3 );
|
||||
color1.toArray( colors.array, ( count + 1 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 2 ) * 3 );
|
||||
|
||||
count += 3;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
positions.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
|
||||
normals.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
|
||||
colors.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
|
||||
|
||||
}
|
||||
|
||||
function stroke( position1, position2, matrix1, matrix2, size1, size2 ) {
|
||||
|
||||
if ( position1.distanceToSquared( position2 ) === 0 ) return;
|
||||
|
||||
let count = geometry.drawRange.count;
|
||||
|
||||
const points1 = getPoints( size1 );
|
||||
const points2 = getPoints( size2 );
|
||||
|
||||
for ( let i = 0, il = points2.length; i < il; i ++ ) {
|
||||
|
||||
const vertex1_2 = points2[ i ];
|
||||
const vertex2_2 = points2[ ( i + 1 ) % il ];
|
||||
const vertex1_1 = points1[ i ];
|
||||
const vertex2_1 = points1[ ( i + 1 ) % il ];
|
||||
|
||||
vector1.copy( vertex1_2 ).applyMatrix4( matrix2 ).add( position2 );
|
||||
vector2.copy( vertex2_2 ).applyMatrix4( matrix2 ).add( position2 );
|
||||
vector3.copy( vertex2_1 ).applyMatrix4( matrix1 ).add( position1 );
|
||||
vector4.copy( vertex1_1 ).applyMatrix4( matrix1 ).add( position1 );
|
||||
|
||||
vector1.toArray( positions.array, ( count + 0 ) * 3 );
|
||||
vector2.toArray( positions.array, ( count + 1 ) * 3 );
|
||||
vector4.toArray( positions.array, ( count + 2 ) * 3 );
|
||||
|
||||
vector2.toArray( positions.array, ( count + 3 ) * 3 );
|
||||
vector3.toArray( positions.array, ( count + 4 ) * 3 );
|
||||
vector4.toArray( positions.array, ( count + 5 ) * 3 );
|
||||
|
||||
vector1.copy( vertex1_2 ).applyMatrix4( matrix2 ).normalize();
|
||||
vector2.copy( vertex2_2 ).applyMatrix4( matrix2 ).normalize();
|
||||
vector3.copy( vertex2_1 ).applyMatrix4( matrix1 ).normalize();
|
||||
vector4.copy( vertex1_1 ).applyMatrix4( matrix1 ).normalize();
|
||||
|
||||
vector1.toArray( normals.array, ( count + 0 ) * 3 );
|
||||
vector2.toArray( normals.array, ( count + 1 ) * 3 );
|
||||
vector4.toArray( normals.array, ( count + 2 ) * 3 );
|
||||
|
||||
vector2.toArray( normals.array, ( count + 3 ) * 3 );
|
||||
vector3.toArray( normals.array, ( count + 4 ) * 3 );
|
||||
vector4.toArray( normals.array, ( count + 5 ) * 3 );
|
||||
|
||||
color2.toArray( colors.array, ( count + 0 ) * 3 );
|
||||
color2.toArray( colors.array, ( count + 1 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 2 ) * 3 );
|
||||
|
||||
color2.toArray( colors.array, ( count + 3 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 4 ) * 3 );
|
||||
color1.toArray( colors.array, ( count + 5 ) * 3 );
|
||||
|
||||
count += 6;
|
||||
|
||||
}
|
||||
|
||||
geometry.drawRange.count = count;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const direction = new Vector3();
|
||||
const normal = new Vector3();
|
||||
const side = new Vector3();
|
||||
|
||||
const point1 = new Vector3();
|
||||
const point2 = new Vector3();
|
||||
|
||||
const matrix1 = new Matrix4();
|
||||
const matrix2 = new Matrix4();
|
||||
|
||||
const lastNormal = new Vector3();
|
||||
const prevDirection = new Vector3();
|
||||
const rotationAxis = new Vector3();
|
||||
|
||||
let isFirstSegment = true;
|
||||
|
||||
let endCapStartIndex = null;
|
||||
let endCapVertexCount = 0;
|
||||
|
||||
function calculateRMF() {
|
||||
|
||||
if ( isFirstSegment === true ) {
|
||||
|
||||
if ( Math.abs( direction.y ) < 0.99 ) {
|
||||
|
||||
vector.copy( direction ).multiplyScalar( direction.y );
|
||||
normal.set( 0, 1, 0 ).sub( vector ).normalize();
|
||||
|
||||
} else {
|
||||
|
||||
vector.copy( direction ).multiplyScalar( direction.x );
|
||||
normal.set( 1, 0, 0 ).sub( vector ).normalize();
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
rotationAxis.crossVectors( prevDirection, direction );
|
||||
|
||||
const rotAxisLength = rotationAxis.length();
|
||||
|
||||
if ( rotAxisLength > 0.0001 ) {
|
||||
|
||||
rotationAxis.divideScalar( rotAxisLength );
|
||||
vector.addVectors( prevDirection, direction );
|
||||
const c1 = - 2.0 / ( 1.0 + prevDirection.dot( direction ) );
|
||||
const dot = lastNormal.dot( vector );
|
||||
normal.copy( lastNormal ).addScaledVector( vector, c1 * dot );
|
||||
|
||||
} else {
|
||||
|
||||
normal.copy( lastNormal );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
side.crossVectors( direction, normal ).normalize();
|
||||
normal.crossVectors( side, direction ).normalize();
|
||||
|
||||
if ( isFirstSegment === false ) {
|
||||
|
||||
const smoothFactor = 0.3;
|
||||
|
||||
normal.lerp( lastNormal, smoothFactor ).normalize();
|
||||
side.crossVectors( direction, normal ).normalize();
|
||||
normal.crossVectors( side, direction ).normalize();
|
||||
|
||||
}
|
||||
|
||||
lastNormal.copy( normal );
|
||||
prevDirection.copy( direction );
|
||||
|
||||
matrix1.makeBasis( side, normal, vector.copy( direction ).negate() );
|
||||
|
||||
}
|
||||
|
||||
function moveTo( position ) {
|
||||
|
||||
point2.copy( position );
|
||||
|
||||
lastNormal.set( 0, 1, 0 );
|
||||
|
||||
isFirstSegment = true;
|
||||
|
||||
endCapStartIndex = null;
|
||||
endCapVertexCount = 0;
|
||||
|
||||
}
|
||||
|
||||
function lineTo( position ) {
|
||||
|
||||
point1.copy( position );
|
||||
|
||||
direction.subVectors( point1, point2 );
|
||||
|
||||
const length = direction.length();
|
||||
|
||||
if ( length === 0 ) return;
|
||||
|
||||
direction.normalize();
|
||||
|
||||
calculateRMF();
|
||||
|
||||
if ( isFirstSegment === true ) {
|
||||
|
||||
color2.copy( color1 );
|
||||
size2 = size1;
|
||||
|
||||
matrix2.copy( matrix1 );
|
||||
|
||||
addCap( point2, matrix2, false, size2 );
|
||||
|
||||
// End cap is added immediately after start cap and updated in-place
|
||||
endCapStartIndex = geometry.drawRange.count;
|
||||
addCap( point1, matrix1, true, size1 );
|
||||
endCapVertexCount = geometry.drawRange.count - endCapStartIndex;
|
||||
|
||||
}
|
||||
|
||||
stroke( point1, point2, matrix1, matrix2, size1, size2 );
|
||||
|
||||
updateEndCap( point1, matrix1, size1 );
|
||||
|
||||
point2.copy( point1 );
|
||||
matrix2.copy( matrix1 );
|
||||
|
||||
color2.copy( color1 );
|
||||
size2 = size1;
|
||||
|
||||
isFirstSegment = false;
|
||||
|
||||
}
|
||||
|
||||
function setSize( value ) {
|
||||
|
||||
size1 = value;
|
||||
|
||||
}
|
||||
|
||||
function setColor( value ) {
|
||||
|
||||
color1.copy( value );
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
let count = 0;
|
||||
|
||||
function update() {
|
||||
|
||||
const start = count;
|
||||
const end = geometry.drawRange.count;
|
||||
|
||||
if ( start === end ) return;
|
||||
|
||||
positions.addUpdateRange( start * 3, ( end - start ) * 3 );
|
||||
positions.needsUpdate = true;
|
||||
|
||||
normals.addUpdateRange( start * 3, ( end - start ) * 3 );
|
||||
normals.needsUpdate = true;
|
||||
|
||||
colors.addUpdateRange( start * 3, ( end - start ) * 3 );
|
||||
colors.needsUpdate = true;
|
||||
|
||||
count = end;
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* The "painted" tube mesh. Must be added to the scene.
|
||||
*
|
||||
* @name TubePainter#mesh
|
||||
* @type {Mesh}
|
||||
*/
|
||||
mesh: mesh,
|
||||
|
||||
/**
|
||||
* Moves the current painting position to the given value.
|
||||
*
|
||||
* @method
|
||||
* @name TubePainter#moveTo
|
||||
* @param {Vector3} position The new painting position.
|
||||
*/
|
||||
moveTo: moveTo,
|
||||
|
||||
/**
|
||||
* Draw a stroke from the current position to the given one.
|
||||
* This method extends the tube while drawing with the XR
|
||||
* controllers.
|
||||
*
|
||||
* @method
|
||||
* @name TubePainter#lineTo
|
||||
* @param {Vector3} position The destination position.
|
||||
*/
|
||||
lineTo: lineTo,
|
||||
|
||||
/**
|
||||
* Sets the size of newly rendered tube segments.
|
||||
*
|
||||
* @method
|
||||
* @name TubePainter#setSize
|
||||
* @param {number} size The size.
|
||||
*/
|
||||
setSize: setSize,
|
||||
|
||||
/**
|
||||
* Sets the color of newly rendered tube segments.
|
||||
*
|
||||
* @method
|
||||
* @name TubePainter#setColor
|
||||
* @param {Color} color The color.
|
||||
*/
|
||||
setColor: setColor,
|
||||
|
||||
/**
|
||||
* Updates the internal geometry buffers so the new painted
|
||||
* segments are rendered.
|
||||
*
|
||||
* @method
|
||||
* @name TubePainter#update
|
||||
*/
|
||||
update: update
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export { TubePainter };
|
||||
520
node_modules/three/examples/jsm/misc/Volume.js
generated
vendored
Normal file
520
node_modules/three/examples/jsm/misc/Volume.js
generated
vendored
Normal file
@@ -0,0 +1,520 @@
|
||||
import {
|
||||
Matrix3,
|
||||
Matrix4,
|
||||
Vector3
|
||||
} from 'three';
|
||||
import { VolumeSlice } from '../misc/VolumeSlice.js';
|
||||
|
||||
/**
|
||||
* This class had been written to handle the output of the {@link NRRDLoader}.
|
||||
* It contains a volume of data and information about it. For now it only handles 3 dimensional data.
|
||||
*
|
||||
* @three_import import { Volume } from 'three/addons/misc/Volume.js';
|
||||
*/
|
||||
class Volume {
|
||||
|
||||
/**
|
||||
* Constructs a new volume.
|
||||
*
|
||||
* @param {number} [xLength] - Width of the volume.
|
||||
* @param {number} [yLength] - Length of the volume.
|
||||
* @param {number} [zLength] - Depth of the volume.
|
||||
* @param {string} [type] - The type of data (uint8, uint16, ...).
|
||||
* @param {ArrayBuffer} [arrayBuffer] - The buffer with volume data.
|
||||
*/
|
||||
constructor( xLength, yLength, zLength, type, arrayBuffer ) {
|
||||
|
||||
if ( xLength !== undefined ) {
|
||||
|
||||
/**
|
||||
* Width of the volume in the IJK coordinate system.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.xLength = Number( xLength ) || 1;
|
||||
|
||||
/**
|
||||
* Height of the volume in the IJK coordinate system.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.yLength = Number( yLength ) || 1;
|
||||
|
||||
/**
|
||||
* Depth of the volume in the IJK coordinate system.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.zLength = Number( zLength ) || 1;
|
||||
|
||||
/**
|
||||
* The order of the Axis dictated by the NRRD header
|
||||
*
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
this.axisOrder = [ 'x', 'y', 'z' ];
|
||||
|
||||
/**
|
||||
* The data of the volume.
|
||||
*
|
||||
* @type {TypedArray}
|
||||
*/
|
||||
this.data;
|
||||
|
||||
switch ( type ) {
|
||||
|
||||
case 'Uint8' :
|
||||
case 'uint8' :
|
||||
case 'uchar' :
|
||||
case 'unsigned char' :
|
||||
case 'uint8_t' :
|
||||
this.data = new Uint8Array( arrayBuffer );
|
||||
break;
|
||||
case 'Int8' :
|
||||
case 'int8' :
|
||||
case 'signed char' :
|
||||
case 'int8_t' :
|
||||
this.data = new Int8Array( arrayBuffer );
|
||||
break;
|
||||
case 'Int16' :
|
||||
case 'int16' :
|
||||
case 'short' :
|
||||
case 'short int' :
|
||||
case 'signed short' :
|
||||
case 'signed short int' :
|
||||
case 'int16_t' :
|
||||
this.data = new Int16Array( arrayBuffer );
|
||||
break;
|
||||
case 'Uint16' :
|
||||
case 'uint16' :
|
||||
case 'ushort' :
|
||||
case 'unsigned short' :
|
||||
case 'unsigned short int' :
|
||||
case 'uint16_t' :
|
||||
this.data = new Uint16Array( arrayBuffer );
|
||||
break;
|
||||
case 'Int32' :
|
||||
case 'int32' :
|
||||
case 'int' :
|
||||
case 'signed int' :
|
||||
case 'int32_t' :
|
||||
this.data = new Int32Array( arrayBuffer );
|
||||
break;
|
||||
case 'Uint32' :
|
||||
case 'uint32' :
|
||||
case 'uint' :
|
||||
case 'unsigned int' :
|
||||
case 'uint32_t' :
|
||||
this.data = new Uint32Array( arrayBuffer );
|
||||
break;
|
||||
case 'longlong' :
|
||||
case 'long long' :
|
||||
case 'long long int' :
|
||||
case 'signed long long' :
|
||||
case 'signed long long int' :
|
||||
case 'int64' :
|
||||
case 'int64_t' :
|
||||
case 'ulonglong' :
|
||||
case 'unsigned long long' :
|
||||
case 'unsigned long long int' :
|
||||
case 'uint64' :
|
||||
case 'uint64_t' :
|
||||
throw new Error( 'Error in Volume constructor : this type is not supported in JavaScript' );
|
||||
case 'Float32' :
|
||||
case 'float32' :
|
||||
case 'float' :
|
||||
this.data = new Float32Array( arrayBuffer );
|
||||
break;
|
||||
case 'Float64' :
|
||||
case 'float64' :
|
||||
case 'double' :
|
||||
this.data = new Float64Array( arrayBuffer );
|
||||
break;
|
||||
default :
|
||||
this.data = new Uint8Array( arrayBuffer );
|
||||
|
||||
}
|
||||
|
||||
if ( this.data.length !== this.xLength * this.yLength * this.zLength ) {
|
||||
|
||||
throw new Error( 'Error in Volume constructor, lengths are not matching arrayBuffer size' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Spacing to apply to the volume from IJK to RAS coordinate system
|
||||
*
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
this.spacing = [ 1, 1, 1 ];
|
||||
|
||||
/**
|
||||
* Offset of the volume in the RAS coordinate system
|
||||
*
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
this.offset = [ 0, 0, 0 ];
|
||||
|
||||
/**
|
||||
* The IJK to RAS matrix.
|
||||
*
|
||||
* @type {Martrix3}
|
||||
*/
|
||||
this.matrix = new Matrix3();
|
||||
this.matrix.identity();
|
||||
|
||||
/**
|
||||
* The RAS to IJK matrix.
|
||||
*
|
||||
* @type {Martrix3}
|
||||
*/
|
||||
this.inverseMatrix = new Matrix3();
|
||||
|
||||
let lowerThreshold = - Infinity;
|
||||
Object.defineProperty( this, 'lowerThreshold', {
|
||||
get: function () {
|
||||
|
||||
return lowerThreshold;
|
||||
|
||||
},
|
||||
/**
|
||||
* The voxels with values under this threshold won't appear in the slices.
|
||||
* If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume.
|
||||
*
|
||||
* @name Volume#lowerThreshold
|
||||
* @type {number}
|
||||
* @param {number} value
|
||||
*/
|
||||
set: function ( value ) {
|
||||
|
||||
lowerThreshold = value;
|
||||
this.sliceList.forEach( function ( slice ) {
|
||||
|
||||
slice.geometryNeedsUpdate = true;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
let upperThreshold = Infinity;
|
||||
Object.defineProperty( this, 'upperThreshold', {
|
||||
get: function () {
|
||||
|
||||
return upperThreshold;
|
||||
|
||||
},
|
||||
/**
|
||||
* The voxels with values over this threshold won't appear in the slices.
|
||||
* If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
|
||||
*
|
||||
* @name Volume#upperThreshold
|
||||
* @type {number}
|
||||
* @param {number} value
|
||||
*/
|
||||
set: function ( value ) {
|
||||
|
||||
upperThreshold = value;
|
||||
this.sliceList.forEach( function ( slice ) {
|
||||
|
||||
slice.geometryNeedsUpdate = true;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
/**
|
||||
* The list of all the slices associated to this volume
|
||||
*
|
||||
* @type {Array<VolumeSlice>}
|
||||
*/
|
||||
this.sliceList = [];
|
||||
|
||||
/**
|
||||
* Whether to use segmentation mode or not.
|
||||
* It can load 16-bits nrrds correctly.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.segmentation = false;
|
||||
|
||||
|
||||
/**
|
||||
* This array holds the dimensions of the volume in the RAS space
|
||||
*
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
this.RASDimensions = [];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for data[access(i,j,k)].
|
||||
*
|
||||
* @param {number} i - First coordinate.
|
||||
* @param {number} j - Second coordinate.
|
||||
* @param {number} k - Third coordinate.
|
||||
* @returns {number} The value in the data array.
|
||||
*/
|
||||
getData( i, j, k ) {
|
||||
|
||||
return this.data[ k * this.xLength * this.yLength + j * this.xLength + i ];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the index in the data array corresponding to the given coordinates in IJK system.
|
||||
*
|
||||
* @param {number} i - First coordinate.
|
||||
* @param {number} j - Second coordinate.
|
||||
* @param {number} k - Third coordinate.
|
||||
* @returns {number} The index.
|
||||
*/
|
||||
access( i, j, k ) {
|
||||
|
||||
return k * this.xLength * this.yLength + j * this.xLength + i;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the IJK coordinates of the voxel corresponding of the given index in the data.
|
||||
*
|
||||
* @param {number} index - Index of the voxel.
|
||||
* @returns {Array<number>} The IJK coordinates as `[x,y,z]`.
|
||||
*/
|
||||
reverseAccess( index ) {
|
||||
|
||||
const z = Math.floor( index / ( this.yLength * this.xLength ) );
|
||||
const y = Math.floor( ( index - z * this.yLength * this.xLength ) / this.xLength );
|
||||
const x = index - z * this.yLength * this.xLength - y * this.xLength;
|
||||
return [ x, y, z ];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a function to all the voxels, be careful, the value will be replaced.
|
||||
*
|
||||
* @param {Function} functionToMap A function to apply to every voxel, will be called with the following parameters:
|
||||
* value of the voxel, index of the voxel, the data (TypedArray).
|
||||
* @param {Object} context - You can specify a context in which call the function, default if this Volume.
|
||||
* @returns {Volume} A reference to this instance.
|
||||
*/
|
||||
map( functionToMap, context ) {
|
||||
|
||||
const length = this.data.length;
|
||||
context = context || this;
|
||||
|
||||
for ( let i = 0; i < length; i ++ ) {
|
||||
|
||||
this.data[ i ] = functionToMap.call( context, this.data[ i ], i, this.data );
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the orientation of the slice and returns all the information relative to the geometry such as sliceAccess,
|
||||
* the plane matrix (orientation and position in RAS coordinate) and the dimensions of the plane in both coordinate system.
|
||||
*
|
||||
* @param {('x'|'y'|'z')} axis - The normal axis to the slice.
|
||||
* @param {number} RASIndex - The index of the slice.
|
||||
* @returns {Object} An object containing all the useful information on the geometry of the slice.
|
||||
*/
|
||||
extractPerpendicularPlane( axis, RASIndex ) {
|
||||
|
||||
let firstSpacing,
|
||||
secondSpacing,
|
||||
positionOffset,
|
||||
IJKIndex;
|
||||
|
||||
const axisInIJK = new Vector3(),
|
||||
firstDirection = new Vector3(),
|
||||
secondDirection = new Vector3(),
|
||||
planeMatrix = ( new Matrix4() ).identity(),
|
||||
volume = this;
|
||||
|
||||
const dimensions = new Vector3( this.xLength, this.yLength, this.zLength );
|
||||
|
||||
|
||||
switch ( axis ) {
|
||||
|
||||
case 'x' :
|
||||
axisInIJK.set( 1, 0, 0 );
|
||||
firstDirection.set( 0, 0, - 1 );
|
||||
secondDirection.set( 0, - 1, 0 );
|
||||
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
|
||||
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
|
||||
IJKIndex = new Vector3( RASIndex, 0, 0 );
|
||||
|
||||
planeMatrix.multiply( ( new Matrix4() ).makeRotationY( Math.PI / 2 ) );
|
||||
positionOffset = ( volume.RASDimensions[ 0 ] - 1 ) / 2;
|
||||
planeMatrix.setPosition( new Vector3( RASIndex - positionOffset, 0, 0 ) );
|
||||
break;
|
||||
case 'y' :
|
||||
axisInIJK.set( 0, 1, 0 );
|
||||
firstDirection.set( 1, 0, 0 );
|
||||
secondDirection.set( 0, 0, 1 );
|
||||
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
|
||||
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
|
||||
IJKIndex = new Vector3( 0, RASIndex, 0 );
|
||||
|
||||
planeMatrix.multiply( ( new Matrix4() ).makeRotationX( - Math.PI / 2 ) );
|
||||
positionOffset = ( volume.RASDimensions[ 1 ] - 1 ) / 2;
|
||||
planeMatrix.setPosition( new Vector3( 0, RASIndex - positionOffset, 0 ) );
|
||||
break;
|
||||
case 'z' :
|
||||
default :
|
||||
axisInIJK.set( 0, 0, 1 );
|
||||
firstDirection.set( 1, 0, 0 );
|
||||
secondDirection.set( 0, - 1, 0 );
|
||||
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
|
||||
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
|
||||
IJKIndex = new Vector3( 0, 0, RASIndex );
|
||||
|
||||
positionOffset = ( volume.RASDimensions[ 2 ] - 1 ) / 2;
|
||||
planeMatrix.setPosition( new Vector3( 0, 0, RASIndex - positionOffset ) );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if ( ! this.segmentation ) {
|
||||
|
||||
firstDirection.applyMatrix4( volume.inverseMatrix ).normalize();
|
||||
secondDirection.applyMatrix4( volume.inverseMatrix ).normalize();
|
||||
axisInIJK.applyMatrix4( volume.inverseMatrix ).normalize();
|
||||
|
||||
}
|
||||
|
||||
firstDirection.arglet = 'i';
|
||||
secondDirection.arglet = 'j';
|
||||
const iLength = Math.floor( Math.abs( firstDirection.dot( dimensions ) ) );
|
||||
const jLength = Math.floor( Math.abs( secondDirection.dot( dimensions ) ) );
|
||||
const planeWidth = Math.abs( iLength * firstSpacing );
|
||||
const planeHeight = Math.abs( jLength * secondSpacing );
|
||||
|
||||
IJKIndex = Math.abs( Math.round( IJKIndex.applyMatrix4( volume.inverseMatrix ).dot( axisInIJK ) ) );
|
||||
const base = [ new Vector3( 1, 0, 0 ), new Vector3( 0, 1, 0 ), new Vector3( 0, 0, 1 ) ];
|
||||
const iDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
|
||||
|
||||
return Math.abs( x.dot( base[ 0 ] ) ) > 0.9;
|
||||
|
||||
} );
|
||||
const jDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
|
||||
|
||||
return Math.abs( x.dot( base[ 1 ] ) ) > 0.9;
|
||||
|
||||
} );
|
||||
const kDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
|
||||
|
||||
return Math.abs( x.dot( base[ 2 ] ) ) > 0.9;
|
||||
|
||||
} );
|
||||
|
||||
function sliceAccess( i, j ) {
|
||||
|
||||
const si = ( iDirection === axisInIJK ) ? IJKIndex : ( iDirection.arglet === 'i' ? i : j );
|
||||
const sj = ( jDirection === axisInIJK ) ? IJKIndex : ( jDirection.arglet === 'i' ? i : j );
|
||||
const sk = ( kDirection === axisInIJK ) ? IJKIndex : ( kDirection.arglet === 'i' ? i : j );
|
||||
|
||||
// invert indices if necessary
|
||||
|
||||
const accessI = ( iDirection.dot( base[ 0 ] ) > 0 ) ? si : ( volume.xLength - 1 ) - si;
|
||||
const accessJ = ( jDirection.dot( base[ 1 ] ) > 0 ) ? sj : ( volume.yLength - 1 ) - sj;
|
||||
const accessK = ( kDirection.dot( base[ 2 ] ) > 0 ) ? sk : ( volume.zLength - 1 ) - sk;
|
||||
|
||||
return volume.access( accessI, accessJ, accessK );
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
iLength: iLength,
|
||||
jLength: jLength,
|
||||
sliceAccess: sliceAccess,
|
||||
matrix: planeMatrix,
|
||||
planeWidth: planeWidth,
|
||||
planeHeight: planeHeight
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a slice corresponding to the given axis and index.
|
||||
* The coordinate are given in the Right Anterior Superior coordinate format.
|
||||
*
|
||||
* @param {('x'|'y'|'z')} axis - The normal axis to the slice.
|
||||
* @param {number} index - The index of the slice.
|
||||
* @returns {VolumeSlice} The extracted slice.
|
||||
*/
|
||||
extractSlice( axis, index ) {
|
||||
|
||||
const slice = new VolumeSlice( this, index, axis );
|
||||
this.sliceList.push( slice );
|
||||
return slice;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Call repaint on all the slices extracted from this volume.
|
||||
*
|
||||
* @see {@link VolumeSlice#repaint}
|
||||
* @returns {Volume} A reference to this volume.
|
||||
*/
|
||||
repaintAllSlices() {
|
||||
|
||||
this.sliceList.forEach( function ( slice ) {
|
||||
|
||||
slice.repaint();
|
||||
|
||||
} );
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the minimum and the maximum of the data in the volume.
|
||||
*
|
||||
* @returns {Array<number>} The min/max data as `[min,max]`.
|
||||
*/
|
||||
computeMinMax() {
|
||||
|
||||
let min = Infinity;
|
||||
let max = - Infinity;
|
||||
|
||||
// buffer the length
|
||||
const datasize = this.data.length;
|
||||
|
||||
let i = 0;
|
||||
|
||||
for ( i = 0; i < datasize; i ++ ) {
|
||||
|
||||
if ( ! isNaN( this.data[ i ] ) ) {
|
||||
|
||||
const value = this.data[ i ];
|
||||
min = Math.min( min, value );
|
||||
max = Math.max( max, value );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
|
||||
return [ min, max ];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Volume };
|
||||
275
node_modules/three/examples/jsm/misc/VolumeSlice.js
generated
vendored
Normal file
275
node_modules/three/examples/jsm/misc/VolumeSlice.js
generated
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
ClampToEdgeWrapping,
|
||||
DoubleSide,
|
||||
LinearFilter,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
PlaneGeometry,
|
||||
Texture,
|
||||
SRGBColorSpace
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* This class has been made to hold a slice of a volume data.
|
||||
*
|
||||
* @see {@link Volume}
|
||||
* @three_import import { VolumeSlice } from 'three/addons/misc/VolumeSlice.js';
|
||||
*/
|
||||
class VolumeSlice {
|
||||
|
||||
/**
|
||||
* Constructs a new volume slice.
|
||||
*
|
||||
* @param {Volume} volume - The associated volume.
|
||||
* @param {number} [index=0] - The index of the slice.
|
||||
* @param {('x'|'y'|'z')} [axis='z'] - For now only 'x', 'y' or 'z' but later it will change to a normal vector.
|
||||
*/
|
||||
constructor( volume, index = 0, axis = 'z' ) {
|
||||
|
||||
const slice = this;
|
||||
|
||||
/**
|
||||
* The associated volume.
|
||||
*
|
||||
* @type {Volume}
|
||||
*/
|
||||
this.volume = volume;
|
||||
|
||||
Object.defineProperty( this, 'index', {
|
||||
get: function () {
|
||||
|
||||
return index;
|
||||
|
||||
},
|
||||
/**
|
||||
* The index of the slice, if changed, will automatically call updateGeometry at the next repaint.
|
||||
*
|
||||
* @name VolumeSlice#index
|
||||
* @type {number}
|
||||
* @default 0
|
||||
* @param {number} value
|
||||
* @return {number}
|
||||
*/
|
||||
set: function ( value ) {
|
||||
|
||||
index = value;
|
||||
slice.geometryNeedsUpdate = true;
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* The normal axis.
|
||||
*
|
||||
* @type {('x'|'y'|'z')}
|
||||
*/
|
||||
this.axis = axis;
|
||||
|
||||
/**
|
||||
* The final canvas used for the texture.
|
||||
*
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
this.canvas = document.createElement( 'canvas' );
|
||||
|
||||
/**
|
||||
* The rendering context of the canvas.
|
||||
*
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.ctx;
|
||||
|
||||
/**
|
||||
* The intermediary canvas used to paint the data.
|
||||
*
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
this.canvasBuffer = document.createElement( 'canvas' );
|
||||
|
||||
/**
|
||||
* The rendering context of the canvas buffer,
|
||||
*
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.ctxBuffer;
|
||||
|
||||
this.updateGeometry();
|
||||
|
||||
|
||||
const canvasMap = new Texture( this.canvas );
|
||||
canvasMap.minFilter = LinearFilter;
|
||||
canvasMap.generateMipmaps = false;
|
||||
canvasMap.wrapS = canvasMap.wrapT = ClampToEdgeWrapping;
|
||||
canvasMap.colorSpace = SRGBColorSpace;
|
||||
const material = new MeshBasicMaterial( { map: canvasMap, side: DoubleSide, transparent: true } );
|
||||
|
||||
/**
|
||||
* The mesh ready to get used in the scene.
|
||||
*
|
||||
* @type {Mesh}
|
||||
*/
|
||||
this.mesh = new Mesh( this.geometry, material );
|
||||
this.mesh.matrixAutoUpdate = false;
|
||||
|
||||
/**
|
||||
* If set to `true`, `updateGeometry()` will be triggered at the next repaint.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.geometryNeedsUpdate = true;
|
||||
this.repaint();
|
||||
|
||||
/**
|
||||
* Width of slice in the original coordinate system, corresponds to the width of the buffer canvas.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 0
|
||||
*/
|
||||
this.iLength = 0;
|
||||
|
||||
/**
|
||||
* Height of slice in the original coordinate system, corresponds to the height of the buffer canvas.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 0
|
||||
*/
|
||||
this.jLength = 0;
|
||||
|
||||
/**
|
||||
* Function that allow the slice to access right data.
|
||||
*
|
||||
* @type {?Function}
|
||||
* @see {@link Volume#extractPerpendicularPlane}
|
||||
*/
|
||||
this.sliceAccess = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the texture and the geometry if geometryNeedsUpdate is set to `true`.
|
||||
*/
|
||||
repaint() {
|
||||
|
||||
if ( this.geometryNeedsUpdate ) {
|
||||
|
||||
this.updateGeometry();
|
||||
|
||||
}
|
||||
|
||||
const iLength = this.iLength,
|
||||
jLength = this.jLength,
|
||||
sliceAccess = this.sliceAccess,
|
||||
volume = this.volume,
|
||||
canvas = this.canvasBuffer,
|
||||
ctx = this.ctxBuffer;
|
||||
|
||||
|
||||
// get the imageData and pixel array from the canvas
|
||||
const imgData = ctx.getImageData( 0, 0, iLength, jLength );
|
||||
const data = imgData.data;
|
||||
const volumeData = volume.data;
|
||||
const upperThreshold = volume.upperThreshold;
|
||||
const lowerThreshold = volume.lowerThreshold;
|
||||
const windowLow = volume.windowLow;
|
||||
const windowHigh = volume.windowHigh;
|
||||
|
||||
// manipulate some pixel elements
|
||||
let pixelCount = 0;
|
||||
|
||||
if ( volume.dataType === 'label' ) {
|
||||
|
||||
console.error( 'THREE.VolumeSlice.repaint: label are not supported yet' );
|
||||
|
||||
// This part is currently useless but will be used when colortables will be handled
|
||||
|
||||
// for ( let j = 0; j < jLength; j ++ ) {
|
||||
|
||||
// for ( let i = 0; i < iLength; i ++ ) {
|
||||
|
||||
// let label = volumeData[ sliceAccess( i, j ) ];
|
||||
// label = label >= this.colorMap.length ? ( label % this.colorMap.length ) + 1 : label;
|
||||
// const color = this.colorMap[ label ];
|
||||
// data[ 4 * pixelCount ] = ( color >> 24 ) & 0xff;
|
||||
// data[ 4 * pixelCount + 1 ] = ( color >> 16 ) & 0xff;
|
||||
// data[ 4 * pixelCount + 2 ] = ( color >> 8 ) & 0xff;
|
||||
// data[ 4 * pixelCount + 3 ] = color & 0xff;
|
||||
// pixelCount ++;
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
} else {
|
||||
|
||||
for ( let j = 0; j < jLength; j ++ ) {
|
||||
|
||||
for ( let i = 0; i < iLength; i ++ ) {
|
||||
|
||||
let value = volumeData[ sliceAccess( i, j ) ];
|
||||
let alpha = 0xff;
|
||||
//apply threshold
|
||||
alpha = upperThreshold >= value ? ( lowerThreshold <= value ? alpha : 0 ) : 0;
|
||||
//apply window level
|
||||
value = Math.floor( 255 * ( value - windowLow ) / ( windowHigh - windowLow ) );
|
||||
value = value > 255 ? 255 : ( value < 0 ? 0 : value | 0 );
|
||||
|
||||
data[ 4 * pixelCount ] = value;
|
||||
data[ 4 * pixelCount + 1 ] = value;
|
||||
data[ 4 * pixelCount + 2 ] = value;
|
||||
data[ 4 * pixelCount + 3 ] = alpha;
|
||||
pixelCount ++;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ctx.putImageData( imgData, 0, 0 );
|
||||
this.ctx.drawImage( canvas, 0, 0, iLength, jLength, 0, 0, this.canvas.width, this.canvas.height );
|
||||
|
||||
|
||||
this.mesh.material.map.needsUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the geometry according to axis and index.
|
||||
* @see {@link Volume#extractPerpendicularPlane}
|
||||
*/
|
||||
updateGeometry() {
|
||||
|
||||
const extracted = this.volume.extractPerpendicularPlane( this.axis, this.index );
|
||||
this.sliceAccess = extracted.sliceAccess;
|
||||
this.jLength = extracted.jLength;
|
||||
this.iLength = extracted.iLength;
|
||||
this.matrix = extracted.matrix;
|
||||
|
||||
this.canvas.width = extracted.planeWidth;
|
||||
this.canvas.height = extracted.planeHeight;
|
||||
this.canvasBuffer.width = this.iLength;
|
||||
this.canvasBuffer.height = this.jLength;
|
||||
this.ctx = this.canvas.getContext( '2d' );
|
||||
this.ctxBuffer = this.canvasBuffer.getContext( '2d' );
|
||||
|
||||
if ( this.geometry ) this.geometry.dispose(); // dispose existing geometry
|
||||
|
||||
this.geometry = new PlaneGeometry( extracted.planeWidth, extracted.planeHeight );
|
||||
|
||||
if ( this.mesh ) {
|
||||
|
||||
this.mesh.geometry = this.geometry;
|
||||
//reset mesh matrix
|
||||
this.mesh.matrix.identity();
|
||||
this.mesh.applyMatrix4( this.matrix );
|
||||
|
||||
}
|
||||
|
||||
this.geometryNeedsUpdate = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { VolumeSlice };
|
||||
Reference in New Issue
Block a user