前言
Web脚本语言JavaScript入门容易,但是想要熟练掌握却需要几年的学习与实践,还要在弱类型开发语言中习惯于使用模块来构建你的代码,就像小时候玩的乐高积木一样。
应用程序的模块化理念,通过将实现隐藏在一个简单的接口后面,您可以使您的应用程序万无一失且易于使用。它只做它应该做的,没有别的。
通过隐藏实现,我们对使用我们代码的人实施了良好的编码风格。您可以访问的实现越多,它就越有可能成为您以后必须处理的复杂的半生不熟的"修复"。
创建3D场景时,唯一的限制是您的想象力 - 以及您的技术知识深度。
描述3D空间的坐标系和用于在坐标系内移动对象是难点加重点。场景图 用于描述构成我们场景的对象层次结构的结构,向量 用于描述3D空间中的位置(以及许多其他事物) ,还有不少于两种描述旋转的方式:欧拉角Euler angles 和四元数quaternions。
对 three.js 和乐高模型web化相关知识点进行实战。希望能与大家交流技术心得和经验,一起共同进步。涉及的知识点如下:
3D 场景初始化:场景、相机、渲染器
透视相机的位置调整
几何体:BoxGeometry、CylinderGeometry、LatheGeometry
材质:MeshLambertMaterial、MeshPhongMaterial、MeshBasicMaterial
光源:AmbientLight、SpotLightHelper、DirectionalLight
更新材质的纹理:TextureLoader
渲染 3D 文本:TextGeometry、FontLoader
实现物体阴影效果
3D 坐标的计算
物体交互的实现:Raycaster、坐标归一化
3D 资源的销毁释放
补间动画、动画编排
class 等
为了方便demo演示,采用传统的 HTML 单文件importmap、module方式来编写代码。
实践
容器
首先,准备一个空白容器,让它的尺寸与浏览器视窗大小相同,以充分利用屏幕空间。
html
<div id="scene-container"></div>
依赖
对于 JS 脚本,使用 导入映射 配置资源的 CDN 地址,这样就可以像使用 npm 包一样导入相关资源。
javascript
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/",
"lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js",
"@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js",
"canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm"
}
}
</script>
接着就可以引入依赖。
javascript
<script type="module">
import * as THREE from 'three';
import * as TWEEN from '@tweenjs/tween.js';
import confetti from 'canvas-confetti';
import { GUI } from 'lil-gui';
</script>
设计变量、类、方法
定义相关变量
javascript
let container, progressBarDiv;
let camera, scene, renderer, controls, gui, guiData, anLoop;
let model;
const modelFileList = {'Car': './car.txt'}
设计乐高类
javascript
class Ldraw {
constructor(){
// 首次使用构造器实例
if (!(Ldraw.instance instanceof Ldraw)) {
this.init();
}
return Ldraw.instance
}
init() {
//container = document.createElement( 'div' );
//document.body.appendChild( container );
camera = new THREE.PerspectiveCamera( 45, container.clientWidth / container.clientHeight, 1, 10000 );
camera.position.set( 150, 200, 250 );
// renderer
renderer = new THREE.WebGLRenderer( { antialias: true } );
//renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setSize(container.clientWidth, container.clientHeight);
// eslint-disable-next-line no-undef
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// canvas画布绝对定位
//renderer.domElement.style.display = 'black';
//renderer.domElement.style.position = 'absolute';
//renderer.domElement.style.top = '0px';
//renderer.domElement.style.left = '0px';
//renderer.domElement.style.zIndex = -1;
container.appendChild( renderer.domElement );
// scene
const pmremGenerator = new THREE.PMREMGenerator( renderer );
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xdeebed );
scene.environment = pmremGenerator.fromScene( new RoomEnvironment( renderer ) ).texture;
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
anLoop = new Loop(camera, scene, renderer);
// gui
guiData = {
//modelFileName: modelFileList[ 'Car' ],
displayLines: true,
conditionalLines: true,
smoothNormals: true,
buildingStep: 0,
noBuildingSteps: 'No steps.',
flatColors: false,
mergeModel: false
};
window.addEventListener( 'resize', this.onWindowResize );
progressBarDiv = document.createElement( 'div' );
progressBarDiv.innerText = 'Loading...';
progressBarDiv.style.fontSize = '3em';
progressBarDiv.style.color = '#888';
progressBarDiv.style.display = 'block';
progressBarDiv.style.position = 'absolute';
progressBarDiv.style.top = '50%';
progressBarDiv.style.width = '100%';
progressBarDiv.style.textAlign = 'center';
// load materials and then the model
this.reloadObject( true );
}
updateObjectsVisibility() {
model.traverse( c => {
if ( c.isLineSegments ) {
if ( c.isConditionalLine ) {
c.visible = guiData.conditionalLines;
} else {
c.visible = guiData.displayLines;
}
} else if ( c.isGroup ) {
// Hide objects with building step > gui setting
c.visible = c.userData.buildingStep <= guiData.buildingStep;
}
} );
}
reloadObject( resetCamera ) {
if ( model ) {
scene.remove( model );
}
model = null;
this.updateProgressBar( 0 );
this.showProgressBar;
// only smooth when not rendering with flat colors to improve processing time
const lDrawLoader = new LDrawLoader();
lDrawLoader.smoothNormals = guiData.smoothNormals && ! guiData.flatColors;
lDrawLoader.load( './car.txt', ( group2 )=> {
//.setPath( ldrawPath )
//.load( guiData.modelFileName, ( group2 )=> {
if ( model ) {
scene.remove( model );
}
model = group2;
// demonstrate how to use convert to flat colors to better mimic the lego instructions look
if ( guiData.flatColors ) {
const convertMaterial = ( material )=> {
const newMaterial = new THREE.MeshBasicMaterial();
newMaterial.color.copy( material.color );
newMaterial.polygonOffset = material.polygonOffset;
newMaterial.polygonOffsetUnits = material.polygonOffsetUnits;
newMaterial.polygonOffsetFactor = material.polygonOffsetFactor;
newMaterial.opacity = material.opacity;
newMaterial.transparent = material.transparent;
newMaterial.depthWrite = material.depthWrite;
newMaterial.toneMapping = false;
return newMaterial;
}
model.traverse( c => {
if ( c.isMesh ) {
if ( Array.isArray( c.material ) ) {
c.material = c.material.map( convertMaterial );
} else {
c.material = convertMaterial( c.material );
}
}
} );
}
// Merge model geometries by material
if ( guiData.mergeModel ) model = LDrawUtils.mergeObject( model );
// Convert from LDraw coordinates: rotate 180 degrees around OX
model.rotation.x = Math.PI;
scene.add( model );
guiData.buildingStep = model.userData.numBuildingSteps - 1;
this.updateObjectsVisibility;
// Adjust camera and light
const bbox = new THREE.Box3().setFromObject( model );
const size = bbox.getSize( new THREE.Vector3() );
const radius = Math.max( size.x, Math.max( size.y, size.z ) ) * 0.5;
if ( resetCamera ) {
controls.target0.copy( bbox.getCenter( new THREE.Vector3() ) );
controls.position0.set( - 2.3, 1, 2 ).multiplyScalar( radius ).add( controls.target0 );
controls.reset();
}
this.createGUI;
this.hideProgressBar;
}, this.onProgress, this.onError );
//});
}
onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
createGUI() {
if ( gui ) {
gui.destroy();
}
gui = new GUI();
gui.add( guiData, 'modelFileName', modelFileList ).name( 'Model' ).onFinishChange( ()=> {
this.reloadObject( true );
} );
gui.add( guiData, 'flatColors' ).name( 'Flat Colors' ).onChange( ()=> {
this.reloadObject( false );
} );
gui.add( guiData, 'mergeModel' ).name( 'Merge model' ).onChange( ()=> {
this.reloadObject( false );
} );
if ( model.userData.numBuildingSteps > 1 ) {
gui.add( guiData, 'buildingStep', 0, model.userData.numBuildingSteps - 1 ).step( 1 ).name( 'Building step' ).onChange( this.updateObjectsVisibility );
} else {
gui.add( guiData, 'noBuildingSteps' ).name( 'Building step' ).onChange( this.updateObjectsVisibility );
}
const changeNormals = ()=> {
this.reloadObject( false );
}
gui.add( guiData, 'smoothNormals' ).name( 'Smooth Normals' ).onChange( changeNormals );
gui.add( guiData, 'displayLines' ).name( 'Display Lines' ).onChange( this.updateObjectsVisibility );
gui.add( guiData, 'conditionalLines' ).name( 'Conditional Lines' ).onChange( this.updateObjectsVisibility );
}
animate() {
requestAnimationFrame( this.animate );
controls.update();
this.render;
}
render() {
renderer.render( scene, camera );
}
updateProgressBar( fraction ) {
progressBarDiv.innerText = 'Loading... ' + Math.round( fraction * 100, 2 ) + '%';
}
onProgress( xhr ) {
if ( xhr.lengthComputable ) {
this.updateProgressBar( xhr.loaded / xhr.total );
console.log( Math.round( xhr.loaded / xhr.total * 100, 2 ) + '% downloaded' );
}
}
onError( error ) {
const message = 'Error loading model';
progressBarDiv.innerText = message;
console.log( message );
console.error( error );
}
showProgressBar() {
document.body.appendChild( progressBarDiv );
}
hideProgressBar() {
document.body.removeChild( progressBarDiv );
}
start() {
anLoop.start();
}
stop() {
anLoop.stop();
}
tick() {
// Code to update animations will go here
anLoop.tick();
}
}
//export { Ldraw }
创建一个场景(Scene)、一个透视相机(PerspectiveCamera)和一个 WebGL 渲染器(WebGLRenderer),并将渲染器添加到 DOM 中。同时,编写一个渲染函数,使用requestAnimationFrame
方法循环渲染场景。
javascript
import {
EventDispatcher,
MOUSE,
Quaternion,
Spherical,
TOUCH,
Plane,
Ray,
MathUtils,
BackSide,
BoxGeometry,
Mesh,
Scene,
MeshBasicMaterial,
MeshStandardMaterial,
PointLight,
BufferAttribute,
BufferGeometry,
FileLoader,
Group,
LineBasicMaterial,
LineSegments,
Loader,
ShaderMaterial,
SRGBColorSpace,
UniformsLib,
UniformsUtils,
Clock,
Color,
Matrix3,
Matrix4,
PerspectiveCamera,
Vector2,
Vector3,
Vector4,
WebGLRenderTarget,
HalfFloatType,
Float32BufferAttribute,
InstancedBufferAttribute,
InterleavedBuffer,
InterleavedBufferAttribute,
TriangleFanDrawMode,
TriangleStripDrawMode,
TrianglesDrawMode,
} from 'three';
// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
// Orbit - left mouse / touch: one-finger move
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
const _changeEvent = { type: 'change' };
const _startEvent = { type: 'start' };
const _endEvent = { type: 'end' };
const _ray = new Ray();
const _plane = new Plane();
const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
class OrbitControls extends EventDispatcher {
constructor( object, domElement ) {
super();
this.object = object;
this.domElement = domElement;
this.domElement.style.touchAction = 'none'; // disable touch scroll
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new Vector3();
// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect
this.cursor = new Vector3();
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0;
this.maxDistance = Infinity;
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0;
this.maxZoom = Infinity;
// Limit camera target within a spherical area around the cursor
this.minTargetRadius = 0;
this.maxTargetRadius = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
this.minAzimuthAngle = - Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.05;
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.panSpeed = 1.0;
this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
this.zoomToCursor = false;
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
// The four arrow keys
this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };
// Mouse buttons
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
// Touch fingers
this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
// the target DOM element for key events
this._domElementKeyEvents = null;
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.getDistance = function () {
return this.object.position.distanceTo( this.target );
};
this.listenToKeyEvents = function ( domElement ) {
domElement.addEventListener( 'keydown', onKeyDown );
this._domElementKeyEvents = domElement;
};
this.stopListenToKeyEvents = function () {
this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
this._domElementKeyEvents = null;
};
this.saveState = function () {
scope.target0.copy( scope.target );
scope.position0.copy( scope.object.position );
scope.zoom0 = scope.object.zoom;
};
this.reset = function () {
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent( _changeEvent );
scope.update();
state = STATE.NONE;
};
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function () {
const offset = new Vector3();
// so camera.up is the orbit axis
const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
const quatInverse = quat.clone().invert();
const lastPosition = new Vector3();
const lastQuaternion = new Quaternion();
const lastTargetPosition = new Vector3();
const twoPI = 2 * Math.PI;
return function update( deltaTime = null ) {
const position = scope.object.position;
offset.copy( position ).sub( scope.target );
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion( quat );
// angle from z-axis around y-axis
spherical.setFromVector3( offset );
if ( scope.autoRotate && state === STATE.NONE ) {
rotateLeft( getAutoRotationAngle( deltaTime ) );
}
if ( scope.enableDamping ) {
spherical.theta += sphericalDelta.theta * scope.dampingFactor;
spherical.phi += sphericalDelta.phi * scope.dampingFactor;
} else {
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
}
// restrict theta to be between desired limits
let min = scope.minAzimuthAngle;
let max = scope.maxAzimuthAngle;
if ( isFinite( min ) && isFinite( max ) ) {
if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
if ( min <= max ) {
spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
} else {
spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
Math.max( min, spherical.theta ) :
Math.min( max, spherical.theta );
}
}
// restrict phi to be between desired limits
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
spherical.makeSafe();
// move target to panned location
if ( scope.enableDamping === true ) {
scope.target.addScaledVector( panOffset, scope.dampingFactor );
} else {
scope.target.add( panOffset );
}
// Limit the target distance from the cursor to create a sphere around the center of interest
scope.target.sub( scope.cursor );
scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius );
scope.target.add( scope.cursor );
let zoomChanged = false;
// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
// we adjust zoom later in these cases
if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {
spherical.radius = clampDistance( spherical.radius );
} else {
const prevRadius = spherical.radius;
spherical.radius = clampDistance( spherical.radius * scale );
zoomChanged = prevRadius != spherical.radius;
}
offset.setFromSpherical( spherical );
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion( quatInverse );
position.copy( scope.target ).add( offset );
scope.object.lookAt( scope.target );
if ( scope.enableDamping === true ) {
sphericalDelta.theta *= ( 1 - scope.dampingFactor );
sphericalDelta.phi *= ( 1 - scope.dampingFactor );
panOffset.multiplyScalar( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
panOffset.set( 0, 0, 0 );
}
// adjust camera position
if ( scope.zoomToCursor && performCursorZoom ) {
let newRadius = null;
if ( scope.object.isPerspectiveCamera ) {
// move the camera down the pointer ray
// this method avoids floating point error
const prevRadius = offset.length();
newRadius = clampDistance( prevRadius * scale );
const radiusDelta = prevRadius - newRadius;
scope.object.position.addScaledVector( dollyDirection, radiusDelta );
scope.object.updateMatrixWorld();
zoomChanged = !! radiusDelta;
} else if ( scope.object.isOrthographicCamera ) {
// adjust the ortho camera position based on zoom changes
const mouseBefore = new Vector3( mouse.x, mouse.y, 0 );
mouseBefore.unproject( scope.object );
const prevZoom = scope.object.zoom;
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
scope.object.updateProjectionMatrix();
zoomChanged = prevZoom !== scope.object.zoom;
const mouseAfter = new Vector3( mouse.x, mouse.y, 0 );
mouseAfter.unproject( scope.object );
scope.object.position.sub( mouseAfter ).add( mouseBefore );
scope.object.updateMatrixWorld();
newRadius = offset.length();
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
scope.zoomToCursor = false;
}
// handle the placement of the target
if ( newRadius !== null ) {
if ( this.screenSpacePanning ) {
// position the orbit target in front of the new camera position
scope.target.set( 0, 0, - 1 )
.transformDirection( scope.object.matrix )
.multiplyScalar( newRadius )
.add( scope.object.position );
} else {
// get the ray and translation plane to compute target
_ray.origin.copy( scope.object.position );
_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );
// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
// extremely large values
if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {
object.lookAt( scope.target );
} else {
_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );
_ray.intersectPlane( _plane, scope.target );
}
}
}
} else if ( scope.object.isOrthographicCamera ) {
const prevZoom = scope.object.zoom;
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
if ( prevZoom !== scope.object.zoom ) {
scope.object.updateProjectionMatrix();
zoomChanged = true;
}
}
scale = 1;
performCursorZoom = false;
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if ( zoomChanged ||
lastPosition.distanceToSquared( scope.object.position ) > EPS ||
8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||
lastTargetPosition.distanceToSquared( scope.target ) > EPS ) {
scope.dispatchEvent( _changeEvent );
lastPosition.copy( scope.object.position );
lastQuaternion.copy( scope.object.quaternion );
lastTargetPosition.copy( scope.target );
return true;
}
return false;
};
}();
this.dispose = function () {
scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
scope.domElement.removeEventListener( 'pointercancel', onPointerUp );
scope.domElement.removeEventListener( 'wheel', onMouseWheel );
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.removeEventListener( 'keydown', interceptControlDown, { capture: true } );
if ( scope._domElementKeyEvents !== null ) {
scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
scope._domElementKeyEvents = null;
}
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
};
//
// internals
//
const scope = this;
const STATE = {
NONE: - 1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6
};
let state = STATE.NONE;
const EPS = 0.000001;
// current position in spherical coordinates
const spherical = new Spherical();
const sphericalDelta = new Spherical();
let scale = 1;
const panOffset = new Vector3();
const rotateStart = new Vector2();
const rotateEnd = new Vector2();
const rotateDelta = new Vector2();
const panStart = new Vector2();
const panEnd = new Vector2();
const panDelta = new Vector2();
const dollyStart = new Vector2();
const dollyEnd = new Vector2();
const dollyDelta = new Vector2();
const dollyDirection = new Vector3();
const mouse = new Vector2();
let performCursorZoom = false;
const pointers = [];
const pointerPositions = {};
let controlActive = false;
function getAutoRotationAngle( deltaTime ) {
if ( deltaTime !== null ) {
return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime;
} else {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
}
function getZoomScale( delta ) {
const normalizedDelta = Math.abs( delta * 0.01 );
return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta );
}
function rotateLeft( angle ) {
sphericalDelta.theta -= angle;
}
function rotateUp( angle ) {
sphericalDelta.phi -= angle;
}
const panLeft = function () {
const v = new Vector3();
return function panLeft( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
v.multiplyScalar( - distance );
panOffset.add( v );
};
}();
const panUp = function () {
const v = new Vector3();
return function panUp( distance, objectMatrix ) {
if ( scope.screenSpacePanning === true ) {
v.setFromMatrixColumn( objectMatrix, 1 );
} else {
v.setFromMatrixColumn( objectMatrix, 0 );
v.crossVectors( scope.object.up, v );
}
v.multiplyScalar( distance );
panOffset.add( v );
};
}();
// deltaX and deltaY are in pixels; right and down are positive
const pan = function () {
const offset = new Vector3();
return function pan( deltaX, deltaY ) {
const element = scope.domElement;
if ( scope.object.isPerspectiveCamera ) {
// perspective
const position = scope.object.position;
offset.copy( position ).sub( scope.target );
let targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we use only clientHeight here so aspect ratio does not distort speed
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
} else if ( scope.object.isOrthographicCamera ) {
// orthographic
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
} else {
// camera neither orthographic nor perspective
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
scope.enablePan = false;
}
};
}();
function dollyOut( dollyScale ) {
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
scale /= dollyScale;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function dollyIn( dollyScale ) {
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {
scale *= dollyScale;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function updateZoomParameters( x, y ) {
if ( ! scope.zoomToCursor ) {
return;
}
performCursorZoom = true;
const rect = scope.domElement.getBoundingClientRect();
const dx = x - rect.left;
const dy = y - rect.top;
const w = rect.width;
const h = rect.height;
mouse.x = ( dx / w ) * 2 - 1;
mouse.y = - ( dy / h ) * 2 + 1;
dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize();
}
function clampDistance( dist ) {
return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate( event ) {
rotateStart.set( event.clientX, event.clientY );
}
function handleMouseDownDolly( event ) {
updateZoomParameters( event.clientX, event.clientX );
dollyStart.set( event.clientX, event.clientY );
}
function handleMouseDownPan( event ) {
panStart.set( event.clientX, event.clientY );
}
function handleMouseMoveRotate( event ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
const element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleMouseMoveDolly( event ) {
dollyEnd.set( event.clientX, event.clientY );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyOut( getZoomScale( dollyDelta.y ) );
} else if ( dollyDelta.y < 0 ) {
dollyIn( getZoomScale( dollyDelta.y ) );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleMouseMovePan( event ) {
panEnd.set( event.clientX, event.clientY );
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleMouseWheel( event ) {
updateZoomParameters( event.clientX, event.clientY );
if ( event.deltaY < 0 ) {
dollyIn( getZoomScale( event.deltaY ) );
} else if ( event.deltaY > 0 ) {
dollyOut( getZoomScale( event.deltaY ) );
}
scope.update();
}
function handleKeyDown( event ) {
let needsUpdate = false;
switch ( event.code ) {
case scope.keys.UP:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( 0, scope.keyPanSpeed );
}
needsUpdate = true;
break;
case scope.keys.BOTTOM:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( 0, - scope.keyPanSpeed );
}
needsUpdate = true;
break;
case scope.keys.LEFT:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( scope.keyPanSpeed, 0 );
}
needsUpdate = true;
break;
case scope.keys.RIGHT:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );
} else {
pan( - scope.keyPanSpeed, 0 );
}
needsUpdate = true;
break;
}
if ( needsUpdate ) {
// prevent the browser from scrolling on cursor keys
event.preventDefault();
scope.update();
}
}
function handleTouchStartRotate( event ) {
if ( pointers.length === 1 ) {
rotateStart.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
rotateStart.set( x, y );
}
}
function handleTouchStartPan( event ) {
if ( pointers.length === 1 ) {
panStart.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
panStart.set( x, y );
}
}
function handleTouchStartDolly( event ) {
const position = getSecondPointerPosition( event );
const dx = event.pageX - position.x;
const dy = event.pageY - position.y;
const distance = Math.sqrt( dx * dx + dy * dy );
dollyStart.set( 0, distance );
}
function handleTouchStartDollyPan( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enablePan ) handleTouchStartPan( event );
}
function handleTouchStartDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enableRotate ) handleTouchStartRotate( event );
}
function handleTouchMoveRotate( event ) {
if ( pointers.length == 1 ) {
rotateEnd.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
rotateEnd.set( x, y );
}
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
const element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
}
function handleTouchMovePan( event ) {
if ( pointers.length === 1 ) {
panEnd.set( event.pageX, event.pageY );
} else {
const position = getSecondPointerPosition( event );
const x = 0.5 * ( event.pageX + position.x );
const y = 0.5 * ( event.pageY + position.y );
panEnd.set( x, y );
}
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
}
function handleTouchMoveDolly( event ) {
const position = getSecondPointerPosition( event );
const dx = event.pageX - position.x;
const dy = event.pageY - position.y;
const distance = Math.sqrt( dx * dx + dy * dy );
dollyEnd.set( 0, distance );
dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
dollyOut( dollyDelta.y );
dollyStart.copy( dollyEnd );
const centerX = ( event.pageX + position.x ) * 0.5;
const centerY = ( event.pageY + position.y ) * 0.5;
updateZoomParameters( centerX, centerY );
}
function handleTouchMoveDollyPan( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enablePan ) handleTouchMovePan( event );
}
function handleTouchMoveDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enableRotate ) handleTouchMoveRotate( event );
}
//
// event handlers - FSM: listen for events and reset state
//
function onPointerDown( event ) {
if ( scope.enabled === false ) return;
if ( pointers.length === 0 ) {
scope.domElement.setPointerCapture( event.pointerId );
scope.domElement.addEventListener( 'pointermove', onPointerMove );
scope.domElement.addEventListener( 'pointerup', onPointerUp );
}
//
if ( isTrackingPointer( event ) ) return;
//
addPointer( event );
if ( event.pointerType === 'touch' ) {
onTouchStart( event );
} else {
onMouseDown( event );
}
}
function onPointerMove( event ) {
if ( scope.enabled === false ) return;
if ( event.pointerType === 'touch' ) {
onTouchMove( event );
} else {
onMouseMove( event );
}
}
function onPointerUp( event ) {
removePointer( event );
switch ( pointers.length ) {
case 0:
scope.domElement.releasePointerCapture( event.pointerId );
scope.domElement.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.removeEventListener( 'pointerup', onPointerUp );
scope.dispatchEvent( _endEvent );
state = STATE.NONE;
break;
case 1:
const pointerId = pointers[ 0 ];
const position = pointerPositions[ pointerId ];
// minimal placeholder event - allows state correction on pointer-up
onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
break;
}
}
function onMouseDown( event ) {
let mouseAction;
switch ( event.button ) {
case 0:
mouseAction = scope.mouseButtons.LEFT;
break;
case 1:
mouseAction = scope.mouseButtons.MIDDLE;
break;
case 2:
mouseAction = scope.mouseButtons.RIGHT;
break;
default:
mouseAction = - 1;
}
switch ( mouseAction ) {
case MOUSE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseDownDolly( event );
state = STATE.DOLLY;
break;
case MOUSE.ROTATE:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
} else {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
}
break;
case MOUSE.PAN:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
} else {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( _startEvent );
}
}
function onMouseMove( event ) {
switch ( state ) {
case STATE.ROTATE:
if ( scope.enableRotate === false ) return;
handleMouseMoveRotate( event );
break;
case STATE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseMoveDolly( event );
break;
case STATE.PAN:
if ( scope.enablePan === false ) return;
handleMouseMovePan( event );
break;
}
}
function onMouseWheel( event ) {
if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
event.preventDefault();
scope.dispatchEvent( _startEvent );
handleMouseWheel( customWheelEvent( event ) );
scope.dispatchEvent( _endEvent );
}
function customWheelEvent( event ) {
const mode = event.deltaMode;
// minimal wheel event altered to meet delta-zoom demand
const newEvent = {
clientX: event.clientX,
clientY: event.clientY,
deltaY: event.deltaY,
};
switch ( mode ) {
case 1: // LINE_MODE
newEvent.deltaY *= 16;
break;
case 2: // PAGE_MODE
newEvent.deltaY *= 100;
break;
}
// detect if event was triggered by pinching
if ( event.ctrlKey && ! controlActive ) {
newEvent.deltaY *= 10;
}
return newEvent;
}
function interceptControlDown( event ) {
if ( event.key === 'Control' ) {
controlActive = true;
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
}
}
function interceptControlUp( event ) {
if ( event.key === 'Control' ) {
controlActive = false;
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );
}
}
function onKeyDown( event ) {
if ( scope.enabled === false || scope.enablePan === false ) return;
handleKeyDown( event );
}
function onTouchStart( event ) {
trackPointer( event );
switch ( pointers.length ) {
case 1:
switch ( scope.touches.ONE ) {
case TOUCH.ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchStartRotate( event );
state = STATE.TOUCH_ROTATE;
break;
case TOUCH.PAN:
if ( scope.enablePan === false ) return;
handleTouchStartPan( event );
state = STATE.TOUCH_PAN;
break;
default:
state = STATE.NONE;
}
break;
case 2:
switch ( scope.touches.TWO ) {
case TOUCH.DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchStartDollyPan( event );
state = STATE.TOUCH_DOLLY_PAN;
break;
case TOUCH.DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchStartDollyRotate( event );
state = STATE.TOUCH_DOLLY_ROTATE;
break;
default:
state = STATE.NONE;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( _startEvent );
}
}
function onTouchMove( event ) {
trackPointer( event );
switch ( state ) {
case STATE.TOUCH_ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchMoveRotate( event );
scope.update();
break;
case STATE.TOUCH_PAN:
if ( scope.enablePan === false ) return;
handleTouchMovePan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchMoveDollyPan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchMoveDollyRotate( event );
scope.update();
break;
default:
state = STATE.NONE;
}
}
function onContextMenu( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
}
function addPointer( event ) {
pointers.push( event.pointerId );
}
function removePointer( event ) {
delete pointerPositions[ event.pointerId ];
for ( let i = 0; i < pointers.length; i ++ ) {
if ( pointers[ i ] == event.pointerId ) {
pointers.splice( i, 1 );
return;
}
}
}
function isTrackingPointer( event ) {
for ( let i = 0; i < pointers.length; i ++ ) {
if ( pointers[ i ] == event.pointerId ) return true;
}
return false;
}
function trackPointer( event ) {
let position = pointerPositions[ event.pointerId ];
if ( position === undefined ) {
position = new Vector2();
pointerPositions[ event.pointerId ] = position;
}
position.set( event.pageX, event.pageY );
}
function getSecondPointerPosition( event ) {
const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ];
return pointerPositions[ pointerId ];
}
//
scope.domElement.addEventListener( 'contextmenu', onContextMenu );
scope.domElement.addEventListener( 'pointerdown', onPointerDown );
scope.domElement.addEventListener( 'pointercancel', onPointerUp );
scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
const document = scope.domElement.getRootNode(); // offscreen canvas compatibility
document.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } );
// force an update at start
this.update();
}
}
//export { OrbitControls };
class RoomEnvironment extends Scene {
constructor( renderer = null ) {
super();
const geometry = new BoxGeometry();
geometry.deleteAttribute( 'uv' );
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
const boxMaterial = new MeshStandardMaterial();
let intensity = 5;
if ( renderer !== null && renderer._useLegacyLights === false ) intensity = 900;
const mainLight = new PointLight( 0xffffff, intensity, 28, 2 );
mainLight.position.set( 0.418, 16.199, 0.300 );
this.add( mainLight );
const room = new Mesh( geometry, roomMaterial );
room.position.set( - 0.757, 13.219, 0.717 );
room.scale.set( 31.713, 28.305, 28.591 );
this.add( room );
const box1 = new Mesh( geometry, boxMaterial );
box1.position.set( - 10.906, 2.009, 1.846 );
box1.rotation.set( 0, - 0.195, 0 );
box1.scale.set( 2.328, 7.905, 4.651 );
this.add( box1 );
const box2 = new Mesh( geometry, boxMaterial );
box2.position.set( - 5.607, - 0.754, - 0.758 );
box2.rotation.set( 0, 0.994, 0 );
box2.scale.set( 1.970, 1.534, 3.955 );
this.add( box2 );
const box3 = new Mesh( geometry, boxMaterial );
box3.position.set( 6.167, 0.857, 7.803 );
box3.rotation.set( 0, 0.561, 0 );
box3.scale.set( 3.927, 6.285, 3.687 );
this.add( box3 );
const box4 = new Mesh( geometry, boxMaterial );
box4.position.set( - 2.017, 0.018, 6.124 );
box4.rotation.set( 0, 0.333, 0 );
box4.scale.set( 2.002, 4.566, 2.064 );
this.add( box4 );
const box5 = new Mesh( geometry, boxMaterial );
box5.position.set( 2.291, - 0.756, - 2.621 );
box5.rotation.set( 0, - 0.286, 0 );
box5.scale.set( 1.546, 1.552, 1.496 );
this.add( box5 );
const box6 = new Mesh( geometry, boxMaterial );
box6.position.set( - 2.193, - 0.369, - 5.547 );
box6.rotation.set( 0, 0.516, 0 );
box6.scale.set( 3.875, 3.487, 2.986 );
this.add( box6 );
// -x right
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light1.position.set( - 16.116, 14.37, 8.208 );
light1.scale.set( 0.1, 2.428, 2.739 );
this.add( light1 );
// -x left
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
light2.position.set( - 16.109, 18.021, - 8.207 );
light2.scale.set( 0.1, 2.425, 2.751 );
this.add( light2 );
// +x
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
light3.position.set( 14.904, 12.198, - 1.832 );
light3.scale.set( 0.15, 4.265, 6.331 );
this.add( light3 );
// +z
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
light4.position.set( - 0.462, 8.89, 14.520 );
light4.scale.set( 4.38, 5.441, 0.088 );
this.add( light4 );
// -z
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
light5.position.set( 3.235, 11.486, - 12.541 );
light5.scale.set( 2.5, 2.0, 0.1 );
this.add( light5 );
// +y
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
light6.position.set( 0.0, 20.0, 0.0 );
light6.scale.set( 1.0, 0.1, 1.0 );
this.add( light6 );
}
dispose() {
const resources = new Set();
this.traverse( ( object ) => {
if ( object.isMesh ) {
resources.add( object.geometry );
resources.add( object.material );
}
} );
for ( const resource of resources ) {
resource.dispose();
}
}
}
function createAreaLightMaterial( intensity ) {
const material = new MeshBasicMaterial();
material.color.setScalar( intensity );
return material;
}
//export { RoomEnvironment };
// Special surface finish tag types.
// Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented
const FINISH_TYPE_DEFAULT = 0;
const FINISH_TYPE_CHROME = 1;
const FINISH_TYPE_PEARLESCENT = 2;
const FINISH_TYPE_RUBBER = 3;
const FINISH_TYPE_MATTE_METALLIC = 4;
const FINISH_TYPE_METAL = 5;
// State machine to search a subobject path.
// The LDraw standard establishes these various possible subfolders.
const FILE_LOCATION_TRY_PARTS = 0;
const FILE_LOCATION_TRY_P = 1;
const FILE_LOCATION_TRY_MODELS = 2;
const FILE_LOCATION_AS_IS = 3;
const FILE_LOCATION_TRY_RELATIVE = 4;
const FILE_LOCATION_TRY_ABSOLUTE = 5;
const FILE_LOCATION_NOT_FOUND = 6;
const MAIN_COLOUR_CODE = '16';
const MAIN_EDGE_COLOUR_CODE = '24';
const COLOR_SPACE_LDRAW = SRGBColorSpace;
const _tempVec0 = new Vector3();
const _tempVec1 = new Vector3();
class LDrawConditionalLineMaterial extends ShaderMaterial {
constructor( parameters ) {
super( {
uniforms: UniformsUtils.merge( [
UniformsLib.fog,
{
diffuse: {
value: new Color()
},
opacity: {
value: 1.0
}
}
] ),
vertexShader: /* glsl */`
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
varying float discardFlag;
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <color_vertex>
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );
c0.xy /= c0.w;
c1.xy /= c1.w;
p0.xy /= p0.w;
p1.xy /= p1.w;
// Get the direction of the segment and an orthogonal vector
vec2 dir = p1.xy - p0.xy;
vec2 norm = vec2( -dir.y, dir.x );
// Get control point directions from the line
vec2 c0dir = c0.xy - p1.xy;
vec2 c1dir = c1.xy - p1.xy;
// If the vectors to the controls points are pointed in different directions away
// from the line segment then the line should not be drawn.
float d0 = dot( normalize( norm ), normalize( c0dir ) );
float d1 = dot( normalize( norm ), normalize( c1dir ) );
discardFlag = float( sign( d0 ) != sign( d1 ) );
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
}
`,
fragmentShader: /* glsl */`
uniform vec3 diffuse;
uniform float opacity;
varying float discardFlag;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
if ( discardFlag > 0.5 ) discard;
#include <clipping_planes_fragment>
vec3 outgoingLight = vec3( 0.0 );
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
outgoingLight = diffuseColor.rgb; // simple shader
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <colorspace_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`,
} );
Object.defineProperties( this, {
opacity: {
get: function () {
return this.uniforms.opacity.value;
},
set: function ( value ) {
this.uniforms.opacity.value = value;
}
},
color: {
get: function () {
return this.uniforms.diffuse.value;
}
}
} );
this.setValues( parameters );
this.isLDrawConditionalLineMaterial = true;
}
}
class ConditionalLineSegments extends LineSegments {
constructor( geometry, material ) {
super( geometry, material );
this.isConditionalLine = true;
}
}
function generateFaceNormals( faces ) {
for ( let i = 0, l = faces.length; i < l; i ++ ) {
const face = faces[ i ];
const vertices = face.vertices;
const v0 = vertices[ 0 ];
const v1 = vertices[ 1 ];
const v2 = vertices[ 2 ];
_tempVec0.subVectors( v1, v0 );
_tempVec1.subVectors( v2, v1 );
face.faceNormal = new Vector3()
.crossVectors( _tempVec0, _tempVec1 )
.normalize();
}
}
//const _ray = new Ray();
function smoothNormals( faces, lineSegments, checkSubSegments = false ) {
// NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because
// it allows edges to be smoothed as expected (see minifig arms).
// --
// And the vector values are initialize multiplied by 1 + 1e-10 to account for floating
// point errors on vertices along quantization boundaries. Ie after matrix multiplication
// vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't
// get merged. This added epsilon attempts to push these error values to the same quantized
// value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169.
const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
function hashVertex( v ) {
const x = ~ ~ ( v.x * hashMultiplier );
const y = ~ ~ ( v.y * hashMultiplier );
const z = ~ ~ ( v.z * hashMultiplier );
return `${ x },${ y },${ z }`;
}
function hashEdge( v0, v1 ) {
return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`;
}
// converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected
// onto the original line.
function toNormalizedRay( v0, v1, targetRay ) {
targetRay.direction.subVectors( v1, v0 ).normalize();
const scalar = v0.dot( targetRay.direction );
targetRay.origin.copy( v0 ).addScaledVector( targetRay.direction, - scalar );
return targetRay;
}
function hashRay( ray ) {
return hashEdge( ray.origin, ray.direction );
}
const hardEdges = new Set();
const hardEdgeRays = new Map();
const halfEdgeList = {};
const normals = [];
// Save the list of hard edges by hash
for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
const ls = lineSegments[ i ];
const vertices = ls.vertices;
const v0 = vertices[ 0 ];
const v1 = vertices[ 1 ];
hardEdges.add( hashEdge( v0, v1 ) );
hardEdges.add( hashEdge( v1, v0 ) );
// only generate the hard edge ray map if we're checking subsegments because it's more expensive to check
// and requires more memory.
if ( checkSubSegments ) {
// add both ray directions to the map
const ray = toNormalizedRay( v0, v1, new Ray() );
const rh1 = hashRay( ray );
if ( ! hardEdgeRays.has( rh1 ) ) {
toNormalizedRay( v1, v0, ray );
const rh2 = hashRay( ray );
const info = {
ray,
distances: [],
};
hardEdgeRays.set( rh1, info );
hardEdgeRays.set( rh2, info );
}
// store both segments ends in min, max order in the distances array to check if a face edge is a
// subsegment later.
const info = hardEdgeRays.get( rh1 );
let d0 = info.ray.direction.dot( v0 );
let d1 = info.ray.direction.dot( v1 );
if ( d0 > d1 ) {
[ d0, d1 ] = [ d1, d0 ];
}
info.distances.push( d0, d1 );
}
}
// track the half edges associated with each triangle
for ( let i = 0, l = faces.length; i < l; i ++ ) {
const tri = faces[ i ];
const vertices = tri.vertices;
const vertCount = vertices.length;
for ( let i2 = 0; i2 < vertCount; i2 ++ ) {
const index = i2;
const next = ( i2 + 1 ) % vertCount;
const v0 = vertices[ index ];
const v1 = vertices[ next ];
const hash = hashEdge( v0, v1 );
// don't add the triangle if the edge is supposed to be hard
if ( hardEdges.has( hash ) ) {
continue;
}
// if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds
if ( checkSubSegments ) {
toNormalizedRay( v0, v1, _ray );
const rayHash = hashRay( _ray );
if ( hardEdgeRays.has( rayHash ) ) {
const info = hardEdgeRays.get( rayHash );
const { ray, distances } = info;
let d0 = ray.direction.dot( v0 );
let d1 = ray.direction.dot( v1 );
if ( d0 > d1 ) {
[ d0, d1 ] = [ d1, d0 ];
}
// return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normals
let found = false;
for ( let i = 0, l = distances.length; i < l; i += 2 ) {
if ( d0 >= distances[ i ] && d1 <= distances[ i + 1 ] ) {
found = true;
break;
}
}
if ( found ) {
continue;
}
}
}
const info = {
index: index,
tri: tri
};
halfEdgeList[ hash ] = info;
}
}
// Iterate until we've tried to connect all faces to share normals
while ( true ) {
// Stop if there are no more faces left
let halfEdge = null;
for ( const key in halfEdgeList ) {
halfEdge = halfEdgeList[ key ];
break;
}
if ( halfEdge === null ) {
break;
}
// Exhaustively find all connected faces
const queue = [ halfEdge ];
while ( queue.length > 0 ) {
// initialize all vertex normals in this triangle
const tri = queue.pop().tri;
const vertices = tri.vertices;
const vertNormals = tri.normals;
const faceNormal = tri.faceNormal;
// Check if any edge is connected to another triangle edge
const vertCount = vertices.length;
for ( let i2 = 0; i2 < vertCount; i2 ++ ) {
const index = i2;
const next = ( i2 + 1 ) % vertCount;
const v0 = vertices[ index ];
const v1 = vertices[ next ];
// delete this triangle from the list so it won't be found again
const hash = hashEdge( v0, v1 );
delete halfEdgeList[ hash ];
const reverseHash = hashEdge( v1, v0 );
const otherInfo = halfEdgeList[ reverseHash ];
if ( otherInfo ) {
const otherTri = otherInfo.tri;
const otherIndex = otherInfo.index;
const otherNormals = otherTri.normals;
const otherVertCount = otherNormals.length;
const otherFaceNormal = otherTri.faceNormal;
// NOTE: If the angle between faces is > 67.5 degrees then assume it's
// hard edge. There are some cases where the line segments do not line up exactly
// with or span multiple triangle edges (see Lunar Vehicle wheels).
if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) {
continue;
}
// if this triangle has already been traversed then it won't be in
// the halfEdgeList. If it has not then add it to the queue and delete
// it so it won't be found again.
if ( reverseHash in halfEdgeList ) {
queue.push( otherInfo );
delete halfEdgeList[ reverseHash ];
}
// share the first normal
const otherNext = ( otherIndex + 1 ) % otherVertCount;
if (
vertNormals[ index ] && otherNormals[ otherNext ] &&
vertNormals[ index ] !== otherNormals[ otherNext ]
) {
otherNormals[ otherNext ].norm.add( vertNormals[ index ].norm );
vertNormals[ index ].norm = otherNormals[ otherNext ].norm;
}
let sharedNormal1 = vertNormals[ index ] || otherNormals[ otherNext ];
if ( sharedNormal1 === null ) {
// it's possible to encounter an edge of a triangle that has already been traversed meaning
// both edges already have different normals defined and shared. To work around this we create
// a wrapper object so when those edges are merged the normals can be updated everywhere.
sharedNormal1 = { norm: new Vector3() };
normals.push( sharedNormal1.norm );
}
if ( vertNormals[ index ] === null ) {
vertNormals[ index ] = sharedNormal1;
sharedNormal1.norm.add( faceNormal );
}
if ( otherNormals[ otherNext ] === null ) {
otherNormals[ otherNext ] = sharedNormal1;
sharedNormal1.norm.add( otherFaceNormal );
}
// share the second normal
if (
vertNormals[ next ] && otherNormals[ otherIndex ] &&
vertNormals[ next ] !== otherNormals[ otherIndex ]
) {
otherNormals[ otherIndex ].norm.add( vertNormals[ next ].norm );
vertNormals[ next ].norm = otherNormals[ otherIndex ].norm;
}
let sharedNormal2 = vertNormals[ next ] || otherNormals[ otherIndex ];
if ( sharedNormal2 === null ) {
sharedNormal2 = { norm: new Vector3() };
normals.push( sharedNormal2.norm );
}
if ( vertNormals[ next ] === null ) {
vertNormals[ next ] = sharedNormal2;
sharedNormal2.norm.add( faceNormal );
}
if ( otherNormals[ otherIndex ] === null ) {
otherNormals[ otherIndex ] = sharedNormal2;
sharedNormal2.norm.add( otherFaceNormal );
}
}
}
}
}
// The normals of each face have been added up so now we average them by normalizing the vector.
for ( let i = 0, l = normals.length; i < l; i ++ ) {
normals[ i ].normalize();
}
}
function isPartType( type ) {
return type === 'Part' || type === 'Unofficial_Part';
}
function isPrimitiveType( type ) {
return /primitive/i.test( type ) || type === 'Subpart';
}
class LineParser {
constructor( line, lineNumber ) {
this.line = line;
this.lineLength = line.length;
this.currentCharIndex = 0;
this.currentChar = ' ';
this.lineNumber = lineNumber;
}
seekNonSpace() {
while ( this.currentCharIndex < this.lineLength ) {
this.currentChar = this.line.charAt( this.currentCharIndex );
if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {
return;
}
this.currentCharIndex ++;
}
}
getToken() {
const pos0 = this.currentCharIndex ++;
// Seek space
while ( this.currentCharIndex < this.lineLength ) {
this.currentChar = this.line.charAt( this.currentCharIndex );
if ( this.currentChar === ' ' || this.currentChar === '\t' ) {
break;
}
this.currentCharIndex ++;
}
const pos1 = this.currentCharIndex;
this.seekNonSpace();
return this.line.substring( pos0, pos1 );
}
getVector() {
return new Vector3( parseFloat( this.getToken() ), parseFloat( this.getToken() ), parseFloat( this.getToken() ) );
}
getRemainingString() {
return this.line.substring( this.currentCharIndex, this.lineLength );
}
isAtTheEnd() {
return this.currentCharIndex >= this.lineLength;
}
setToEnd() {
this.currentCharIndex = this.lineLength;
}
getLineNumberString() {
return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : '';
}
}
// Fetches and parses an intermediate representation of LDraw parts files.
class LDrawParsedCache {
constructor( loader ) {
this.loader = loader;
this._cache = {};
}
cloneResult( original ) {
const result = {};
// vertices are transformed and normals computed before being converted to geometry
// so these pieces must be cloned.
result.faces = original.faces.map( face => {
return {
colorCode: face.colorCode,
material: face.material,
vertices: face.vertices.map( v => v.clone() ),
normals: face.normals.map( () => null ),
faceNormal: null
};
} );
result.conditionalSegments = original.conditionalSegments.map( face => {
return {
colorCode: face.colorCode,
material: face.material,
vertices: face.vertices.map( v => v.clone() ),
controlPoints: face.controlPoints.map( v => v.clone() )
};
} );
result.lineSegments = original.lineSegments.map( face => {
return {
colorCode: face.colorCode,
material: face.material,
vertices: face.vertices.map( v => v.clone() )
};
} );
// none if this is subsequently modified
result.type = original.type;
result.category = original.category;
result.keywords = original.keywords;
result.author = original.author;
result.subobjects = original.subobjects;
result.fileName = original.fileName;
result.totalFaces = original.totalFaces;
result.startingBuildingStep = original.startingBuildingStep;
result.materials = original.materials;
result.group = null;
return result;
}
async fetchData( fileName ) {
let triedLowerCase = false;
let locationState = FILE_LOCATION_TRY_PARTS;
while ( locationState !== FILE_LOCATION_NOT_FOUND ) {
let subobjectURL = fileName;
switch ( locationState ) {
case FILE_LOCATION_AS_IS:
locationState = locationState + 1;
break;
case FILE_LOCATION_TRY_PARTS:
subobjectURL = 'parts/' + subobjectURL;
locationState = locationState + 1;
break;
case FILE_LOCATION_TRY_P:
subobjectURL = 'p/' + subobjectURL;
locationState = locationState + 1;
break;
case FILE_LOCATION_TRY_MODELS:
subobjectURL = 'models/' + subobjectURL;
locationState = locationState + 1;
break;
case FILE_LOCATION_TRY_RELATIVE:
subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;
locationState = locationState + 1;
break;
case FILE_LOCATION_TRY_ABSOLUTE:
if ( triedLowerCase ) {
// Try absolute path
locationState = FILE_LOCATION_NOT_FOUND;
} else {
// Next attempt is lower case
fileName = fileName.toLowerCase();
subobjectURL = fileName;
triedLowerCase = true;
locationState = FILE_LOCATION_TRY_PARTS;
}
break;
}
const loader = this.loader;
const fileLoader = new FileLoader( loader.manager );
fileLoader.setPath( loader.partsLibraryPath );
fileLoader.setRequestHeader( loader.requestHeader );
fileLoader.setWithCredentials( loader.withCredentials );
try {
const text = await fileLoader.loadAsync( subobjectURL );
return text;
} catch ( _ ) {
continue;
}
}
throw new Error( 'LDrawLoader: Subobject "' + fileName + '" could not be loaded.' );
}
parse( text, fileName = null ) {
const loader = this.loader;
// final results
const faces = [];
const lineSegments = [];
const conditionalSegments = [];
const subobjects = [];
const materials = {};
const getLocalMaterial = colorCode => {
return materials[ colorCode ] || null;
};
let type = 'Model';
let category = null;
let keywords = null;
let author = null;
let totalFaces = 0;
// split into lines
if ( text.indexOf( '\r\n' ) !== - 1 ) {
// This is faster than String.split with regex that splits on both
text = text.replace( /\r\n/g, '\n' );
}
const lines = text.split( '\n' );
const numLines = lines.length;
let parsingEmbeddedFiles = false;
let currentEmbeddedFileName = null;
let currentEmbeddedText = null;
let bfcCertified = false;
let bfcCCW = true;
let bfcInverted = false;
let bfcCull = true;
let startingBuildingStep = false;
try{
// Parse all line commands
for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
const line = lines[ lineIndex ];
if ( line.length === 0 ) continue;
if ( parsingEmbeddedFiles ) {
if ( line.startsWith( '0 FILE ' ) ) {
// Save previous embedded file in the cache
this.setData( currentEmbeddedFileName, currentEmbeddedText );
// New embedded text file
currentEmbeddedFileName = line.substring( 7 );
currentEmbeddedText = '';
} else {
currentEmbeddedText += line + '\n';
}
continue;
}
const lp = new LineParser( line, lineIndex + 1 );
lp.seekNonSpace();
if ( lp.isAtTheEnd() ) {
// Empty line
continue;
}
// Parse the line type
const lineType = lp.getToken();
let material;
let colorCode;
let segment;
let ccw;
let doubleSided;
let v0, v1, v2, v3, c0, c1;
switch ( lineType ) {
// Line type 0: Comment or META
case '0':
// Parse meta directive
const meta = lp.getToken();
if ( meta ) {
switch ( meta ) {
case '!LDRAW_ORG':
type = lp.getToken();
break;
case '!COLOUR':
material = loader.parseColorMetaDirective( lp );
if ( material ) {
materials[ material.userData.code ] = material;
} else {
console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
}
break;
case '!CATEGORY':
category = lp.getToken();
break;
case '!KEYWORDS':
const newKeywords = lp.getRemainingString().split( ',' );
if ( newKeywords.length > 0 ) {
if ( ! keywords ) {
keywords = [];
}
newKeywords.forEach( function ( keyword ) {
keywords.push( keyword.trim() );
} );
}
break;
case 'FILE':
if ( lineIndex > 0 ) {
// Start embedded text files parsing
parsingEmbeddedFiles = true;
currentEmbeddedFileName = lp.getRemainingString();
currentEmbeddedText = '';
bfcCertified = false;
bfcCCW = true;
}
break;
case 'BFC':
// Changes to the backface culling state
while ( ! lp.isAtTheEnd() ) {
const token = lp.getToken();
switch ( token ) {
case 'CERTIFY':
case 'NOCERTIFY':
bfcCertified = token === 'CERTIFY';
bfcCCW = true;
break;
case 'CW':
case 'CCW':
bfcCCW = token === 'CCW';
break;
case 'INVERTNEXT':
bfcInverted = true;
break;
case 'CLIP':
case 'NOCLIP':
bfcCull = token === 'CLIP';
break;
default:
console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );
break;
}
}
break;
case 'STEP':
startingBuildingStep = true;
break;
case 'Author:':
author = lp.getToken();
break;
default:
// Other meta directives are not implemented
break;
}
}
break;
// Line type 1: Sub-object file
case '1':
colorCode = lp.getToken();
material = getLocalMaterial( colorCode );
const posX = parseFloat( lp.getToken() );
const posY = parseFloat( lp.getToken() );
const posZ = parseFloat( lp.getToken() );
const m0 = parseFloat( lp.getToken() );
const m1 = parseFloat( lp.getToken() );
const m2 = parseFloat( lp.getToken() );
const m3 = parseFloat( lp.getToken() );
const m4 = parseFloat( lp.getToken() );
const m5 = parseFloat( lp.getToken() );
const m6 = parseFloat( lp.getToken() );
const m7 = parseFloat( lp.getToken() );
const m8 = parseFloat( lp.getToken() );
const matrix = new Matrix4().set(
m0, m1, m2, posX,
m3, m4, m5, posY,
m6, m7, m8, posZ,
0, 0, 0, 1
);
let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );
if ( loader.fileMap[ fileName ] ) {
// Found the subobject path in the preloaded file path map
fileName = loader.fileMap[ fileName ];
} else {
// Standardized subfolders
if ( fileName.startsWith( 's/' ) ) {
fileName = 'parts/' + fileName;
} else if ( fileName.startsWith( '48/' ) ) {
fileName = 'p/' + fileName;
}
}
subobjects.push( {
material: material,
colorCode: colorCode,
matrix: matrix,
fileName: fileName,
inverted: bfcInverted,
startingBuildingStep: startingBuildingStep
} );
startingBuildingStep = false;
bfcInverted = false;
break;
// Line type 2: Line segment
case '2':
colorCode = lp.getToken();
material = getLocalMaterial( colorCode );
v0 = lp.getVector();
v1 = lp.getVector();
segment = {
material: material,
colorCode: colorCode,
vertices: [ v0, v1 ],
};
lineSegments.push( segment );
break;
// Line type 5: Conditional Line segment
case '5':
colorCode = lp.getToken();
material = getLocalMaterial( colorCode );
v0 = lp.getVector();
v1 = lp.getVector();
c0 = lp.getVector();
c1 = lp.getVector();
segment = {
material: material,
colorCode: colorCode,
vertices: [ v0, v1 ],
controlPoints: [ c0, c1 ],
};
conditionalSegments.push( segment );
break;
// Line type 3: Triangle
case '3':
colorCode = lp.getToken();
material = getLocalMaterial( colorCode );
ccw = bfcCCW;
doubleSided = ! bfcCertified || ! bfcCull;
if ( ccw === true ) {
v0 = lp.getVector();
v1 = lp.getVector();
v2 = lp.getVector();
} else {
v2 = lp.getVector();
v1 = lp.getVector();
v0 = lp.getVector();
}
faces.push( {
material: material,
colorCode: colorCode,
faceNormal: null,
vertices: [ v0, v1, v2 ],
normals: [ null, null, null ],
} );
totalFaces ++;
if ( doubleSided === true ) {
faces.push( {
material: material,
colorCode: colorCode,
faceNormal: null,
vertices: [ v2, v1, v0 ],
normals: [ null, null, null ],
} );
totalFaces ++;
}
break;
// Line type 4: Quadrilateral
case '4':
colorCode = lp.getToken();
material = getLocalMaterial( colorCode );
ccw = bfcCCW;
doubleSided = ! bfcCertified || ! bfcCull;
if ( ccw === true ) {
v0 = lp.getVector();
v1 = lp.getVector();
v2 = lp.getVector();
v3 = lp.getVector();
} else {
v3 = lp.getVector();
v2 = lp.getVector();
v1 = lp.getVector();
v0 = lp.getVector();
}
// specifically place the triangle diagonal in the v0 and v1 slots so we can
// account for the doubling of vertices later when smoothing normals.
faces.push( {
material: material,
colorCode: colorCode,
faceNormal: null,
vertices: [ v0, v1, v2, v3 ],
normals: [ null, null, null, null ],
} );
totalFaces += 2;
if ( doubleSided === true ) {
faces.push( {
material: material,
colorCode: colorCode,
faceNormal: null,
vertices: [ v3, v2, v1, v0 ],
normals: [ null, null, null, null ],
} );
totalFaces += 2;
}
break;
default:
throw new Error( 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.' );
}
}
}catch(error){
console.error(error);
}
if ( parsingEmbeddedFiles ) {
this.setData( currentEmbeddedFileName, currentEmbeddedText );
}
return {
faces,
conditionalSegments,
lineSegments,
type,
category,
keywords,
author,
subobjects,
totalFaces,
startingBuildingStep,
materials,
fileName,
group: null
};
}
// returns an (optionally cloned) instance of the data
getData( fileName, clone = true ) {
const key = fileName.toLowerCase();
const result = this._cache[ key ];
if ( result === null || result instanceof Promise ) {
return null;
}
if ( clone ) {
return this.cloneResult( result );
} else {
return result;
}
}
// kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when
// the data is ready to use and can be retrieved synchronously with "getData".
async ensureDataLoaded( fileName ) {
const key = fileName.toLowerCase();
if ( ! ( key in this._cache ) ) {
// replace the promise with a copy of the parsed data for immediate processing
this._cache[ key ] = this.fetchData( fileName ).then( text => {
const info = this.parse( text, fileName );
this._cache[ key ] = info;
return info;
} );
}
await this._cache[ key ];
}
// sets the data in the cache from parsed data
setData( fileName, text ) {
const key = fileName.toLowerCase();
this._cache[ key ] = this.parse( text, fileName );
}
}
// returns the material for an associated color code. If the color code is 16 for a face or 24 for
// an edge then the passthroughColorCode is used.
function getMaterialFromCode( colorCode, parentColorCode, materialHierarchy, forEdge ) {
const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
if ( isPassthrough ) {
colorCode = parentColorCode;
}
return materialHierarchy[ colorCode ] || null;
}
// Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type.
class LDrawPartsGeometryCache {
constructor( loader ) {
this.loader = loader;
this.parseCache = new LDrawParsedCache( loader );
this._cache = {};
}
// Convert the given file information into a mesh by processing subobjects.
async processIntoMesh( info ) {
const loader = this.loader;
const parseCache = this.parseCache;
const faceMaterials = new Set();
// Processes the part subobject information to load child parts and merge geometry onto part
// piece object.
const processInfoSubobjects = async ( info, subobject = null ) => {
const subobjects = info.subobjects;
const promises = [];
// Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate
// group which lets instruction steps apply correctly.
for ( let i = 0, l = subobjects.length; i < l; i ++ ) {
const subobject = subobjects[ i ];
const promise = parseCache.ensureDataLoaded( subobject.fileName ).then( () => {
const subobjectInfo = parseCache.getData( subobject.fileName, false );
if ( ! isPrimitiveType( subobjectInfo.type ) ) {
return this.loadModel( subobject.fileName ).catch( error => {
console.warn( error );
return null;
} );
}
return processInfoSubobjects( parseCache.getData( subobject.fileName ), subobject );
} );
promises.push( promise );
}
const group = new Group();
group.userData.category = info.category;
group.userData.keywords = info.keywords;
group.userData.author = info.author;
group.userData.type = info.type;
group.userData.fileName = info.fileName;
info.group = group;
const subobjectInfos = await Promise.all( promises );
for ( let i = 0, l = subobjectInfos.length; i < l; i ++ ) {
const subobject = info.subobjects[ i ];
const subobjectInfo = subobjectInfos[ i ];
if ( subobjectInfo === null ) {
// the subobject failed to load
continue;
}
// if the subobject was loaded as a separate group then apply the parent scopes materials
if ( subobjectInfo.isGroup ) {
const subobjectGroup = subobjectInfo;
subobject.matrix.decompose( subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale );
subobjectGroup.userData.startingBuildingStep = subobject.startingBuildingStep;
subobjectGroup.name = subobject.fileName;
loader.applyMaterialsToMesh( subobjectGroup, subobject.colorCode, info.materials );
subobjectGroup.userData.colorCode = subobject.colorCode;
group.add( subobjectGroup );
continue;
}
// add the subobject group if it has children in case it has both children and primitives
if ( subobjectInfo.group.children.length ) {
group.add( subobjectInfo.group );
}
// transform the primitives into the local space of the parent piece and append them to
// to the parent primitives list.
const parentLineSegments = info.lineSegments;
const parentConditionalSegments = info.conditionalSegments;
const parentFaces = info.faces;
const lineSegments = subobjectInfo.lineSegments;
const conditionalSegments = subobjectInfo.conditionalSegments;
const faces = subobjectInfo.faces;
const matrix = subobject.matrix;
const inverted = subobject.inverted;
const matrixScaleInverted = matrix.determinant() < 0;
const colorCode = subobject.colorCode;
const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;
for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
const ls = lineSegments[ i ];
const vertices = ls.vertices;
vertices[ 0 ].applyMatrix4( matrix );
vertices[ 1 ].applyMatrix4( matrix );
ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;
ls.material = ls.material || getMaterialFromCode( ls.colorCode, ls.colorCode, info.materials, true );
parentLineSegments.push( ls );
}
for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
const os = conditionalSegments[ i ];
const vertices = os.vertices;
const controlPoints = os.controlPoints;
vertices[ 0 ].applyMatrix4( matrix );
vertices[ 1 ].applyMatrix4( matrix );
controlPoints[ 0 ].applyMatrix4( matrix );
controlPoints[ 1 ].applyMatrix4( matrix );
os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;
os.material = os.material || getMaterialFromCode( os.colorCode, os.colorCode, info.materials, true );
parentConditionalSegments.push( os );
}
for ( let i = 0, l = faces.length; i < l; i ++ ) {
const tri = faces[ i ];
const vertices = tri.vertices;
for ( let i = 0, l = vertices.length; i < l; i ++ ) {
vertices[ i ].applyMatrix4( matrix );
}
tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;
tri.material = tri.material || getMaterialFromCode( tri.colorCode, colorCode, info.materials, false );
faceMaterials.add( tri.colorCode );
// If the scale of the object is negated then the triangle winding order
// needs to be flipped.
if ( matrixScaleInverted !== inverted ) {
vertices.reverse();
}
parentFaces.push( tri );
}
info.totalFaces += subobjectInfo.totalFaces;
}
// Apply the parent subobjects pass through material code to this object. This is done several times due
// to material scoping.
if ( subobject ) {
loader.applyMaterialsToMesh( group, subobject.colorCode, info.materials );
group.userData.colorCode = subobject.colorCode;
}
return info;
};
// Track material use to see if we need to use the normal smooth slow path for hard edges.
for ( let i = 0, l = info.faces; i < l; i ++ ) {
faceMaterials.add( info.faces[ i ].colorCode );
}
await processInfoSubobjects( info );
if ( loader.smoothNormals ) {
const checkSubSegments = faceMaterials.size > 1;
generateFaceNormals( info.faces );
smoothNormals( info.faces, info.lineSegments, checkSubSegments );
}
// Add the primitive objects and metadata.
const group = info.group;
if ( info.faces.length > 0 ) {
group.add( createObject( this.loader, info.faces, 3, false, info.totalFaces ) );
}
if ( info.lineSegments.length > 0 ) {
group.add( createObject( this.loader, info.lineSegments, 2 ) );
}
if ( info.conditionalSegments.length > 0 ) {
group.add( createObject( this.loader, info.conditionalSegments, 2, true ) );
}
return group;
}
hasCachedModel( fileName ) {
return fileName !== null && fileName.toLowerCase() in this._cache;
}
async getCachedModel( fileName ) {
if ( fileName !== null && this.hasCachedModel( fileName ) ) {
const key = fileName.toLowerCase();
const group = await this._cache[ key ];
return group.clone();
} else {
return null;
}
}
// Loads and parses the model with the given file name. Returns a cached copy if available.
async loadModel( fileName ) {
const parseCache = this.parseCache;
const key = fileName.toLowerCase();
if ( this.hasCachedModel( fileName ) ) {
// Return cached model if available.
return this.getCachedModel( fileName );
} else {
// Otherwise parse a new model.
// Ensure the file data is loaded and pre parsed.
await parseCache.ensureDataLoaded( fileName );
const info = parseCache.getData( fileName );
const promise = this.processIntoMesh( info );
// Now that the file has loaded it's possible that another part parse has been waiting in parallel
// so check the cache again to see if it's been added since the last async operation so we don't
// do unnecessary work.
if ( this.hasCachedModel( fileName ) ) {
return this.getCachedModel( fileName );
}
// Cache object if it's a part so it can be reused later.
if ( isPartType( info.type ) ) {
this._cache[ key ] = promise;
}
// return a copy
const group = await promise;
return group.clone();
}
}
// parses the given model text into a renderable object. Returns cached copy if available.
async parseModel( text ) {
const parseCache = this.parseCache;
const info = parseCache.parse( text );
if ( isPartType( info.type ) && this.hasCachedModel( info.fileName ) ) {
return this.getCachedModel( info.fileName );
}
return this.processIntoMesh( info );
}
}
function sortByMaterial( a, b ) {
if ( a.colorCode === b.colorCode ) {
return 0;
}
if ( a.colorCode < b.colorCode ) {
return - 1;
}
return 1;
}
function createObject( loader, elements, elementSize, isConditionalSegments = false, totalElements = null ) {
// Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )
// With per face / segment material, implemented with mesh groups and materials array
// Sort the faces or line segments by color code to make later the mesh groups
elements.sort( sortByMaterial );
if ( totalElements === null ) {
totalElements = elements.length;
}
const positions = new Float32Array( elementSize * totalElements * 3 );
const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;
const materials = [];
const quadArray = new Array( 6 );
const bufferGeometry = new BufferGeometry();
let prevMaterial = null;
let index0 = 0;
let numGroupVerts = 0;
let offset = 0;
for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
const elem = elements[ iElem ];
let vertices = elem.vertices;
if ( vertices.length === 4 ) {
quadArray[ 0 ] = vertices[ 0 ];
quadArray[ 1 ] = vertices[ 1 ];
quadArray[ 2 ] = vertices[ 2 ];
quadArray[ 3 ] = vertices[ 0 ];
quadArray[ 4 ] = vertices[ 2 ];
quadArray[ 5 ] = vertices[ 3 ];
vertices = quadArray;
}
for ( let j = 0, l = vertices.length; j < l; j ++ ) {
const v = vertices[ j ];
const index = offset + j * 3;
positions[ index + 0 ] = v.x;
positions[ index + 1 ] = v.y;
positions[ index + 2 ] = v.z;
}
// create the normals array if this is a set of faces
if ( elementSize === 3 ) {
if ( ! elem.faceNormal ) {
const v0 = vertices[ 0 ];
const v1 = vertices[ 1 ];
const v2 = vertices[ 2 ];
_tempVec0.subVectors( v1, v0 );
_tempVec1.subVectors( v2, v1 );
elem.faceNormal = new Vector3()
.crossVectors( _tempVec0, _tempVec1 )
.normalize();
}
let elemNormals = elem.normals;
if ( elemNormals.length === 4 ) {
quadArray[ 0 ] = elemNormals[ 0 ];
quadArray[ 1 ] = elemNormals[ 1 ];
quadArray[ 2 ] = elemNormals[ 2 ];
quadArray[ 3 ] = elemNormals[ 0 ];
quadArray[ 4 ] = elemNormals[ 2 ];
quadArray[ 5 ] = elemNormals[ 3 ];
elemNormals = quadArray;
}
for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {
// use face normal if a vertex normal is not provided
let n = elem.faceNormal;
if ( elemNormals[ j ] ) {
n = elemNormals[ j ].norm;
}
const index = offset + j * 3;
normals[ index + 0 ] = n.x;
normals[ index + 1 ] = n.y;
normals[ index + 2 ] = n.z;
}
}
if ( prevMaterial !== elem.colorCode ) {
if ( prevMaterial !== null ) {
bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
}
const material = elem.material;
if ( material !== null ) {
if ( elementSize === 3 ) {
materials.push( material );
} else if ( elementSize === 2 ) {
if ( isConditionalSegments ) {
const edgeMaterial = loader.edgeMaterialCache.get( material );
materials.push( loader.conditionalEdgeMaterialCache.get( edgeMaterial ) );
} else {
materials.push( loader.edgeMaterialCache.get( material ) );
}
}
} else {
// If a material has not been made available yet then keep the color code string in the material array
// to save the spot for the material once a parent scopes materials are being applied to the object.
materials.push( elem.colorCode );
}
prevMaterial = elem.colorCode;
index0 = offset / 3;
numGroupVerts = vertices.length;
} else {
numGroupVerts += vertices.length;
}
offset += 3 * vertices.length;
}
if ( numGroupVerts > 0 ) {
bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
}
bufferGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
if ( normals !== null ) {
bufferGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
}
let object3d = null;
if ( elementSize === 2 ) {
if ( isConditionalSegments ) {
object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
} else {
object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
}
} else if ( elementSize === 3 ) {
object3d = new Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
}
if ( isConditionalSegments ) {
object3d.isConditionalLine = true;
const controlArray0 = new Float32Array( elements.length * 3 * 2 );
const controlArray1 = new Float32Array( elements.length * 3 * 2 );
const directionArray = new Float32Array( elements.length * 3 * 2 );
for ( let i = 0, l = elements.length; i < l; i ++ ) {
const os = elements[ i ];
const vertices = os.vertices;
const controlPoints = os.controlPoints;
const c0 = controlPoints[ 0 ];
const c1 = controlPoints[ 1 ];
const v0 = vertices[ 0 ];
const v1 = vertices[ 1 ];
const index = i * 3 * 2;
controlArray0[ index + 0 ] = c0.x;
controlArray0[ index + 1 ] = c0.y;
controlArray0[ index + 2 ] = c0.z;
controlArray0[ index + 3 ] = c0.x;
controlArray0[ index + 4 ] = c0.y;
controlArray0[ index + 5 ] = c0.z;
controlArray1[ index + 0 ] = c1.x;
controlArray1[ index + 1 ] = c1.y;
controlArray1[ index + 2 ] = c1.z;
controlArray1[ index + 3 ] = c1.x;
controlArray1[ index + 4 ] = c1.y;
controlArray1[ index + 5 ] = c1.z;
directionArray[ index + 0 ] = v1.x - v0.x;
directionArray[ index + 1 ] = v1.y - v0.y;
directionArray[ index + 2 ] = v1.z - v0.z;
directionArray[ index + 3 ] = v1.x - v0.x;
directionArray[ index + 4 ] = v1.y - v0.y;
directionArray[ index + 5 ] = v1.z - v0.z;
}
bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );
bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );
bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );
}
return object3d;
}
//
class LDrawLoader extends Loader {
constructor( manager ) {
super( manager );
// Array of THREE.Material
this.materials = [];
this.materialLibrary = {};
this.edgeMaterialCache = new WeakMap();
this.conditionalEdgeMaterialCache = new WeakMap();
// This also allows to handle the embedded text files ("0 FILE" lines)
this.partsCache = new LDrawPartsGeometryCache( this );
// This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.
this.fileMap = {};
// Initializes the materials library with default materials
this.setMaterials( [] );
// If this flag is set to true the vertex normals will be smoothed.
this.smoothNormals = true;
// The path to load parts from the LDraw parts library from.
this.partsLibraryPath = '';
// Material assigned to not available colors for meshes and edges
this.missingColorMaterial = new MeshStandardMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF, roughness: 0.3, metalness: 0 } );
this.missingEdgeColorMaterial = new LineBasicMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF } );
this.missingConditionalEdgeColorMaterial = new LDrawConditionalLineMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, fog: true, color: 0xFF00FF } );
this.edgeMaterialCache.set( this.missingColorMaterial, this.missingEdgeColorMaterial );
this.conditionalEdgeMaterialCache.set( this.missingEdgeColorMaterial, this.missingConditionalEdgeColorMaterial );
}
setPartsLibraryPath( path ) {
this.partsLibraryPath = path;
return this;
}
async preloadMaterials( url ) {
const fileLoader = new FileLoader( this.manager );
fileLoader.setPath( this.path );
fileLoader.setRequestHeader( this.requestHeader );
fileLoader.setWithCredentials( this.withCredentials );
const text = await fileLoader.loadAsync( url );
const colorLineRegex = /^0 !COLOUR/;
const lines = text.split( /[\n\r]/g );
const materials = [];
for ( let i = 0, l = lines.length; i < l; i ++ ) {
const line = lines[ i ];
if ( colorLineRegex.test( line ) ) {
const directive = line.replace( colorLineRegex, '' );
const material = this.parseColorMetaDirective( new LineParser( directive ) );
materials.push( material );
}
}
this.setMaterials( materials );
}
load( url, onLoad, onProgress, onError ) {
const fileLoader = new FileLoader( this.manager );
fileLoader.setPath( this.path );
fileLoader.setRequestHeader( this.requestHeader );
fileLoader.setWithCredentials( this.withCredentials );
fileLoader.load( url, text => {
this.partsCache
.parseModel( text, this.materialLibrary )
.then( group => {
this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );
this.computeBuildingSteps( group );
group.userData.fileName = url;
onLoad( group );
} )
.catch( onError );
}, onProgress, onError );
}
parse( text, onLoad ) {
this.partsCache
.parseModel( text, this.materialLibrary )
.then( group => {
this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );
this.computeBuildingSteps( group );
group.userData.fileName = '';
onLoad( group );
} );
}
setMaterials( materials ) {
this.materialLibrary = {};
this.materials = [];
for ( let i = 0, l = materials.length; i < l; i ++ ) {
this.addMaterial( materials[ i ] );
}
// Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ) );
this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) );
return this;
}
setFileMap( fileMap ) {
this.fileMap = fileMap;
return this;
}
addMaterial( material ) {
// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
const matLib = this.materialLibrary;
if ( ! matLib[ material.userData.code ] ) {
this.materials.push( material );
matLib[ material.userData.code ] = material;
}
return this;
}
getMaterial( colorCode ) {
if ( colorCode.startsWith( '0x2' ) ) {
// Special 'direct' material value (RGB color)
const color = colorCode.substring( 3 );
return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );
}
return this.materialLibrary[ colorCode ] || null;
}
// Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present
// in the material array if they need to be filled in.
applyMaterialsToMesh( group, parentColorCode, materialHierarchy, finalMaterialPass = false ) {
// find any missing materials as indicated by a color code string and replace it with a material from the current material lib
const loader = this;
const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;
group.traverse( c => {
if ( c.isMesh || c.isLineSegments ) {
if ( Array.isArray( c.material ) ) {
for ( let i = 0, l = c.material.length; i < l; i ++ ) {
if ( ! c.material[ i ].isMaterial ) {
c.material[ i ] = getMaterial( c, c.material[ i ] );
}
}
} else if ( ! c.material.isMaterial ) {
c.material = getMaterial( c, c.material );
}
}
} );
// Returns the appropriate material for the object (line or face) given color code. If the code is "pass through"
// (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's
// simply returned for the subsequent material application.
function getMaterial( c, colorCode ) {
// if our parent is a passthrough color code and we don't have the current material color available then
// return early.
if ( parentIsPassthrough && ! ( colorCode in materialHierarchy ) && ! finalMaterialPass ) {
return colorCode;
}
const forEdge = c.isLineSegments || c.isConditionalLine;
const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
if ( isPassthrough ) {
colorCode = parentColorCode;
}
let material = null;
if ( colorCode in materialHierarchy ) {
material = materialHierarchy[ colorCode ];
} else if ( finalMaterialPass ) {
// see if we can get the final material from from the "getMaterial" function which will attempt to
// parse the "direct" colors
material = loader.getMaterial( colorCode );
if ( material === null ) {
// otherwise throw a warning if this is final opportunity to set the material
console.warn( `LDrawLoader: Material properties for code ${ colorCode } not available.` );
// And return the 'missing color' material
material = loader.missingColorMaterial;
}
} else {
return colorCode;
}
if ( c.isLineSegments ) {
material = loader.edgeMaterialCache.get( material );
if ( c.isConditionalLine ) {
material = loader.conditionalEdgeMaterialCache.get( material );
}
}
return material;
}
}
getMainMaterial() {
return this.getMaterial( MAIN_COLOUR_CODE );
}
getMainEdgeMaterial() {
const mat = this.getMaterial( MAIN_EDGE_COLOUR_CODE );
return mat ? this.edgeMaterialCache.get( mat ) : null;
}
parseColorMetaDirective( lineParser ) {
// Parses a color definition and returns a THREE.Material
let code = null;
// Triangle and line colors
let fillColor = '#FF00FF';
let edgeColor = '#FF00FF';
// Transparency
let alpha = 1;
let isTransparent = false;
// Self-illumination:
let luminance = 0;
let finishType = FINISH_TYPE_DEFAULT;
let edgeMaterial = null;
const name = lineParser.getToken();
if ( ! name ) {
throw new Error( 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.' );
}
// Parse tag tokens and their parameters
let token = null;
while ( true ) {
token = lineParser.getToken();
if ( ! token ) {
break;
}
if ( ! parseLuminance( token ) ) {
switch ( token.toUpperCase() ) {
case 'CODE':
code = lineParser.getToken();
break;
case 'VALUE':
fillColor = lineParser.getToken();
if ( fillColor.startsWith( '0x' ) ) {
fillColor = '#' + fillColor.substring( 2 );
} else if ( ! fillColor.startsWith( '#' ) ) {
throw new Error( 'LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.' );
}
break;
case 'EDGE':
edgeColor = lineParser.getToken();
if ( edgeColor.startsWith( '0x' ) ) {
edgeColor = '#' + edgeColor.substring( 2 );
} else if ( ! edgeColor.startsWith( '#' ) ) {
// Try to see if edge color is a color code
edgeMaterial = this.getMaterial( edgeColor );
if ( ! edgeMaterial ) {
throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' );
}
// Get the edge material for this triangle material
edgeMaterial = this.edgeMaterialCache.get( edgeMaterial );
}
break;
case 'ALPHA':
alpha = parseInt( lineParser.getToken() );
if ( isNaN( alpha ) ) {
throw new Error( 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.' );
}
alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );
if ( alpha < 1 ) {
isTransparent = true;
}
break;
case 'LUMINANCE':
if ( ! parseLuminance( lineParser.getToken() ) ) {
throw new Error( 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.' );
}
break;
case 'CHROME':
finishType = FINISH_TYPE_CHROME;
break;
case 'PEARLESCENT':
finishType = FINISH_TYPE_PEARLESCENT;
break;
case 'RUBBER':
finishType = FINISH_TYPE_RUBBER;
break;
case 'MATTE_METALLIC':
finishType = FINISH_TYPE_MATTE_METALLIC;
break;
case 'METAL':
finishType = FINISH_TYPE_METAL;
break;
case 'MATERIAL':
// Not implemented
lineParser.setToEnd();
break;
default:
throw new Error( 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.' );
}
}
}
let material = null;
switch ( finishType ) {
case FINISH_TYPE_DEFAULT:
material = new MeshStandardMaterial( { roughness: 0.3, metalness: 0 } );
break;
case FINISH_TYPE_PEARLESCENT:
// Try to imitate pearlescency by making the surface glossy
material = new MeshStandardMaterial( { roughness: 0.3, metalness: 0.25 } );
break;
case FINISH_TYPE_CHROME:
// Mirror finish surface
material = new MeshStandardMaterial( { roughness: 0, metalness: 1 } );
break;
case FINISH_TYPE_RUBBER:
// Rubber finish
material = new MeshStandardMaterial( { roughness: 0.9, metalness: 0 } );
break;
case FINISH_TYPE_MATTE_METALLIC:
// Brushed metal finish
material = new MeshStandardMaterial( { roughness: 0.8, metalness: 0.4 } );
break;
case FINISH_TYPE_METAL:
// Average metal finish
material = new MeshStandardMaterial( { roughness: 0.2, metalness: 0.85 } );
break;
default:
// Should not happen
break;
}
material.color.setStyle( fillColor, COLOR_SPACE_LDRAW );
material.transparent = isTransparent;
material.premultipliedAlpha = true;
material.opacity = alpha;
material.depthWrite = ! isTransparent;
material.polygonOffset = true;
material.polygonOffsetFactor = 1;
if ( luminance !== 0 ) {
material.emissive.setStyle( fillColor, COLOR_SPACE_LDRAW ).multiplyScalar( luminance );
}
if ( ! edgeMaterial ) {
// This is the material used for edges
edgeMaterial = new LineBasicMaterial( {
color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),
transparent: isTransparent,
opacity: alpha,
depthWrite: ! isTransparent
} );
edgeMaterial.color;
edgeMaterial.userData.code = code;
edgeMaterial.name = name + ' - Edge';
// This is the material used for conditional edges
const conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {
fog: true,
transparent: isTransparent,
depthWrite: ! isTransparent,
color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),
opacity: alpha,
} );
conditionalEdgeMaterial.userData.code = code;
conditionalEdgeMaterial.name = name + ' - Conditional Edge';
this.conditionalEdgeMaterialCache.set( edgeMaterial, conditionalEdgeMaterial );
}
material.userData.code = code;
material.name = name;
this.edgeMaterialCache.set( material, edgeMaterial );
this.addMaterial( material );
return material;
function parseLuminance( token ) {
// Returns success
let lum;
if ( token.startsWith( 'LUMINANCE' ) ) {
lum = parseInt( token.substring( 9 ) );
} else {
lum = parseInt( token );
}
if ( isNaN( lum ) ) {
return false;
}
luminance = Math.max( 0, Math.min( 1, lum / 255 ) );
return true;
}
}
computeBuildingSteps( model ) {
// Sets userdata.buildingStep number in Group objects and userData.numBuildingSteps number in the root Group object.
let stepNumber = 0;
model.traverse( c => {
if ( c.isGroup ) {
if ( c.userData.startingBuildingStep ) {
stepNumber ++;
}
c.userData.buildingStep = stepNumber;
}
} );
model.userData.numBuildingSteps = stepNumber + 1;
}
}
//export { LDrawLoader };
class Reflector extends Mesh {
constructor( geometry, options = {} ) {
super( geometry );
this.isReflector = true;
this.type = 'Reflector';
this.camera = new PerspectiveCamera();
const scope = this;
const color = ( options.color !== undefined ) ? new Color( options.color ) : new Color( 0x7F7F7F );
const textureWidth = options.textureWidth || 512;
const textureHeight = options.textureHeight || 512;
const clipBias = options.clipBias || 0;
const shader = options.shader || Reflector.ReflectorShader;
const multisample = ( options.multisample !== undefined ) ? options.multisample : 4;
//
const reflectorPlane = new Plane();
const normal = new Vector3();
const reflectorWorldPosition = new Vector3();
const cameraWorldPosition = new Vector3();
const rotationMatrix = new Matrix4();
const lookAtPosition = new Vector3( 0, 0, - 1 );
const clipPlane = new Vector4();
const view = new Vector3();
const target = new Vector3();
const q = new Vector4();
const textureMatrix = new Matrix4();
const virtualCamera = this.camera;
const renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, { samples: multisample, type: HalfFloatType } );
const material = new ShaderMaterial( {
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
uniforms: UniformsUtils.clone( shader.uniforms ),
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader
} );
material.uniforms[ 'tDiffuse' ].value = renderTarget.texture;
material.uniforms[ 'color' ].value = color;
material.uniforms[ 'textureMatrix' ].value = textureMatrix;
this.material = material;
this.onBeforeRender = function ( renderer, scene, camera ) {
reflectorWorldPosition.setFromMatrixPosition( scope.matrixWorld );
cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
rotationMatrix.extractRotation( scope.matrixWorld );
normal.set( 0, 0, 1 );
normal.applyMatrix4( rotationMatrix );
view.subVectors( reflectorWorldPosition, cameraWorldPosition );
// Avoid rendering when reflector is facing away
if ( view.dot( normal ) > 0 ) return;
view.reflect( normal ).negate();
view.add( reflectorWorldPosition );
rotationMatrix.extractRotation( camera.matrixWorld );
lookAtPosition.set( 0, 0, - 1 );
lookAtPosition.applyMatrix4( rotationMatrix );
lookAtPosition.add( cameraWorldPosition );
target.subVectors( reflectorWorldPosition, lookAtPosition );
target.reflect( normal ).negate();
target.add( reflectorWorldPosition );
virtualCamera.position.copy( view );
virtualCamera.up.set( 0, 1, 0 );
virtualCamera.up.applyMatrix4( rotationMatrix );
virtualCamera.up.reflect( normal );
virtualCamera.lookAt( target );
virtualCamera.far = camera.far; // Used in WebGLBackground
virtualCamera.updateMatrixWorld();
virtualCamera.projectionMatrix.copy( camera.projectionMatrix );
// Update the texture matrix
textureMatrix.set(
0.5, 0.0, 0.0, 0.5,
0.0, 0.5, 0.0, 0.5,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0
);
textureMatrix.multiply( virtualCamera.projectionMatrix );
textureMatrix.multiply( virtualCamera.matrixWorldInverse );
textureMatrix.multiply( scope.matrixWorld );
// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
reflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition );
reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse );
clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant );
const projectionMatrix = virtualCamera.projectionMatrix;
q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];
q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];
q.z = - 1.0;
q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];
// Calculate the scaled plane vector
clipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) );
// Replacing the third row of the projection matrix
projectionMatrix.elements[ 2 ] = clipPlane.x;
projectionMatrix.elements[ 6 ] = clipPlane.y;
projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias;
projectionMatrix.elements[ 14 ] = clipPlane.w;
// Render
scope.visible = false;
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
renderer.setRenderTarget( renderTarget );
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
if ( renderer.autoClear === false ) renderer.clear();
renderer.render( scene, virtualCamera );
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
renderer.setRenderTarget( currentRenderTarget );
// Restore viewport
const viewport = camera.viewport;
if ( viewport !== undefined ) {
renderer.state.viewport( viewport );
}
scope.visible = true;
};
this.getRenderTarget = function () {
return renderTarget;
};
this.dispose = function () {
renderTarget.dispose();
scope.material.dispose();
};
}
}
Reflector.ReflectorShader = {
name: 'ReflectorShader',
uniforms: {
'color': {
value: null
},
'tDiffuse': {
value: null
},
'textureMatrix': {
value: null
}
},
vertexShader: /* glsl */`
uniform mat4 textureMatrix;
varying vec4 vUv;
#include <common>
#include <logdepthbuf_pars_vertex>
void main() {
vUv = textureMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
#include <logdepthbuf_vertex>
}`,
fragmentShader: /* glsl */`
uniform vec3 color;
uniform sampler2D tDiffuse;
varying vec4 vUv;
#include <logdepthbuf_pars_fragment>
float blendOverlay( float base, float blend ) {
return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
}
vec3 blendOverlay( vec3 base, vec3 blend ) {
return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
}
void main() {
#include <logdepthbuf_fragment>
vec4 base = texture2DProj( tDiffuse, vUv );
gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 );
#include <tonemapping_fragment>
#include <colorspace_fragment>
}`
};
//export { Reflector };
function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
if ( ! MikkTSpace || ! MikkTSpace.isReady ) {
throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );
}
if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {
throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' );
}
function getAttributeArray( attribute ) {
if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) {
const dstArray = new Float32Array( attribute.count * attribute.itemSize );
for ( let i = 0, j = 0; i < attribute.count; i ++ ) {
dstArray[ j ++ ] = attribute.getX( i );
dstArray[ j ++ ] = attribute.getY( i );
if ( attribute.itemSize > 2 ) {
dstArray[ j ++ ] = attribute.getZ( i );
}
}
return dstArray;
}
if ( attribute.array instanceof Float32Array ) {
return attribute.array;
}
return new Float32Array( attribute.array );
}
// MikkTSpace algorithm requires non-indexed input.
const _geometry = geometry.index ? geometry.toNonIndexed() : geometry;
// Compute vertex tangents.
const tangents = MikkTSpace.generateTangents(
getAttributeArray( _geometry.attributes.position ),
getAttributeArray( _geometry.attributes.normal ),
getAttributeArray( _geometry.attributes.uv )
);
// Texture coordinate convention of glTF differs from the apparent
// default of the MikkTSpace library; .w component must be flipped.
if ( negateSign ) {
for ( let i = 3; i < tangents.length; i += 4 ) {
tangents[ i ] *= - 1;
}
}
//
_geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) );
if ( geometry !== _geometry ) {
geometry.copy( _geometry );
}
return geometry;
}
/**
* @param {Array<BufferGeometry>} geometries
* @param {Boolean} useGroups
* @return {BufferGeometry}
*/
function mergeGeometries( geometries, useGroups = false ) {
const isIndexed = geometries[ 0 ].index !== null;
const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );
const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );
const attributes = {};
const morphAttributes = {};
const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;
const mergedGeometry = new BufferGeometry();
let offset = 0;
for ( let i = 0; i < geometries.length; ++ i ) {
const geometry = geometries[ i ];
let attributesCount = 0;
// ensure that all geometries are indexed, or none
if ( isIndexed !== ( geometry.index !== null ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );
return null;
}
// gather attributes, exit early if they're different
for ( const name in geometry.attributes ) {
if ( ! attributesUsed.has( name ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' );
return null;
}
if ( attributes[ name ] === undefined ) attributes[ name ] = [];
attributes[ name ].push( geometry.attributes[ name ] );
attributesCount ++;
}
// ensure geometries have the same number of attributes
if ( attributesCount !== attributesUsed.size ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );
return null;
}
// gather morph attributes, exit early if they're different
if ( morphTargetsRelative !== geometry.morphTargetsRelative ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );
return null;
}
for ( const name in geometry.morphAttributes ) {
if ( ! morphAttributesUsed.has( name ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' );
return null;
}
if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];
morphAttributes[ name ].push( geometry.morphAttributes[ name ] );
}
if ( useGroups ) {
let count;
if ( isIndexed ) {
count = geometry.index.count;
} else if ( geometry.attributes.position !== undefined ) {
count = geometry.attributes.position.count;
} else {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );
return null;
}
mergedGeometry.addGroup( offset, count, i );
offset += count;
}
}
// merge indices
if ( isIndexed ) {
let indexOffset = 0;
const mergedIndex = [];
for ( let i = 0; i < geometries.length; ++ i ) {
const index = geometries[ i ].index;
for ( let j = 0; j < index.count; ++ j ) {
mergedIndex.push( index.getX( j ) + indexOffset );
}
indexOffset += geometries[ i ].attributes.position.count;
}
mergedGeometry.setIndex( mergedIndex );
}
// merge attributes
for ( const name in attributes ) {
const mergedAttribute = mergeAttributes( attributes[ name ] );
if ( ! mergedAttribute ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' );
return null;
}
mergedGeometry.setAttribute( name, mergedAttribute );
}
// merge morph attributes
for ( const name in morphAttributes ) {
const numMorphTargets = morphAttributes[ name ][ 0 ].length;
if ( numMorphTargets === 0 ) break;
mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
mergedGeometry.morphAttributes[ name ] = [];
for ( let i = 0; i < numMorphTargets; ++ i ) {
const morphAttributesToMerge = [];
for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) {
morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );
}
const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge );
if ( ! mergedMorphAttribute ) {
console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );
return null;
}
mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );
}
}
return mergedGeometry;
}
/**
* @param {Array<BufferAttribute>} attributes
* @return {BufferAttribute}
*/
function mergeAttributes( attributes ) {
let TypedArray;
let itemSize;
let normalized;
let gpuType = - 1;
let arrayLength = 0;
for ( let i = 0; i < attributes.length; ++ i ) {
const attribute = attributes[ i ];
if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
if ( TypedArray !== attribute.array.constructor ) {
console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' );
return null;
}
if ( itemSize === undefined ) itemSize = attribute.itemSize;
if ( itemSize !== attribute.itemSize ) {
console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' );
return null;
}
if ( normalized === undefined ) normalized = attribute.normalized;
if ( normalized !== attribute.normalized ) {
console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' );
return null;
}
if ( gpuType === - 1 ) gpuType = attribute.gpuType;
if ( gpuType !== attribute.gpuType ) {
console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' );
return null;
}
arrayLength += attribute.count * itemSize;
}
const array = new TypedArray( arrayLength );
const result = new BufferAttribute( array, itemSize, normalized );
let offset = 0;
for ( let i = 0; i < attributes.length; ++ i ) {
const attribute = attributes[ i ];
if ( attribute.isInterleavedBufferAttribute ) {
const tupleOffset = offset / itemSize;
for ( let j = 0, l = attribute.count; j < l; j ++ ) {
for ( let c = 0; c < itemSize; c ++ ) {
const value = attribute.getComponent( j, c );
result.setComponent( j + tupleOffset, c, value );
}
}
} else {
array.set( attribute.array, offset );
}
offset += attribute.count * itemSize;
}
if ( gpuType !== undefined ) {
result.gpuType = gpuType;
}
return result;
}
/**
* @param {BufferAttribute}
* @return {BufferAttribute}
*/
export function deepCloneAttribute( attribute ) {
if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {
return deinterleaveAttribute( attribute );
}
if ( attribute.isInstancedBufferAttribute ) {
return new InstancedBufferAttribute().copy( attribute );
}
return new BufferAttribute().copy( attribute );
}
/**
* @param {Array<BufferAttribute>} attributes
* @return {Array<InterleavedBufferAttribute>}
*/
function interleaveAttributes( attributes ) {
// Interleaves the provided attributes into an InterleavedBuffer and returns
// a set of InterleavedBufferAttributes for each attribute
let TypedArray;
let arrayLength = 0;
let stride = 0;
// calculate the length and type of the interleavedBuffer
for ( let i = 0, l = attributes.length; i < l; ++ i ) {
const attribute = attributes[ i ];
if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
if ( TypedArray !== attribute.array.constructor ) {
console.error( 'AttributeBuffers of different types cannot be interleaved' );
return null;
}
arrayLength += attribute.array.length;
stride += attribute.itemSize;
}
// Create the set of buffer attributes
const interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride );
let offset = 0;
const res = [];
const getters = [ 'getX', 'getY', 'getZ', 'getW' ];
const setters = [ 'setX', 'setY', 'setZ', 'setW' ];
for ( let j = 0, l = attributes.length; j < l; j ++ ) {
const attribute = attributes[ j ];
const itemSize = attribute.itemSize;
const count = attribute.count;
const iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized );
res.push( iba );
offset += itemSize;
// Move the data for each attribute into the new interleavedBuffer
// at the appropriate offset
for ( let c = 0; c < count; c ++ ) {
for ( let k = 0; k < itemSize; k ++ ) {
iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) );
}
}
}
return res;
}
// returns a new, non-interleaved version of the provided attribute
export function deinterleaveAttribute( attribute ) {
const cons = attribute.data.array.constructor;
const count = attribute.count;
const itemSize = attribute.itemSize;
const normalized = attribute.normalized;
const array = new cons( count * itemSize );
let newAttribute;
if ( attribute.isInstancedInterleavedBufferAttribute ) {
newAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute );
} else {
newAttribute = new BufferAttribute( array, itemSize, normalized );
}
for ( let i = 0; i < count; i ++ ) {
newAttribute.setX( i, attribute.getX( i ) );
if ( itemSize >= 2 ) {
newAttribute.setY( i, attribute.getY( i ) );
}
if ( itemSize >= 3 ) {
newAttribute.setZ( i, attribute.getZ( i ) );
}
if ( itemSize >= 4 ) {
newAttribute.setW( i, attribute.getW( i ) );
}
}
return newAttribute;
}
// deinterleaves all attributes on the geometry
export function deinterleaveGeometry( geometry ) {
const attributes = geometry.attributes;
const morphTargets = geometry.morphTargets;
const attrMap = new Map();
for ( const key in attributes ) {
const attr = attributes[ key ];
if ( attr.isInterleavedBufferAttribute ) {
if ( ! attrMap.has( attr ) ) {
attrMap.set( attr, deinterleaveAttribute( attr ) );
}
attributes[ key ] = attrMap.get( attr );
}
}
for ( const key in morphTargets ) {
const attr = morphTargets[ key ];
if ( attr.isInterleavedBufferAttribute ) {
if ( ! attrMap.has( attr ) ) {
attrMap.set( attr, deinterleaveAttribute( attr ) );
}
morphTargets[ key ] = attrMap.get( attr );
}
}
}
/**
* @param {BufferGeometry} geometry
* @return {number}
*/
function estimateBytesUsed( geometry ) {
// Return the estimated memory used by this geometry in bytes
// Calculate using itemSize, count, and BYTES_PER_ELEMENT to account
// for InterleavedBufferAttributes.
let mem = 0;
for ( const name in geometry.attributes ) {
const attr = geometry.getAttribute( name );
mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;
}
const indices = geometry.getIndex();
mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;
return mem;
}
/**
* @param {BufferGeometry} geometry
* @param {number} tolerance
* @return {BufferGeometry}
*/
function mergeVertices( geometry, tolerance = 1e-4 ) {
tolerance = Math.max( tolerance, Number.EPSILON );
// Generate an index buffer if the geometry doesn't have one, or optimize it
// if it's already available.
const hashToIndex = {};
const indices = geometry.getIndex();
const positions = geometry.getAttribute( 'position' );
const vertexCount = indices ? indices.count : positions.count;
// next value for triangle indices
let nextIndex = 0;
// attributes and new attribute arrays
const attributeNames = Object.keys( geometry.attributes );
const tmpAttributes = {};
const tmpMorphAttributes = {};
const newIndices = [];
const getters = [ 'getX', 'getY', 'getZ', 'getW' ];
const setters = [ 'setX', 'setY', 'setZ', 'setW' ];
// Initialize the arrays, allocating space conservatively. Extra
// space will be trimmed in the last step.
for ( let i = 0, l = attributeNames.length; i < l; i ++ ) {
const name = attributeNames[ i ];
const attr = geometry.attributes[ name ];
tmpAttributes[ name ] = new BufferAttribute(
new attr.array.constructor( attr.count * attr.itemSize ),
attr.itemSize,
attr.normalized
);
const morphAttr = geometry.morphAttributes[ name ];
if ( morphAttr ) {
tmpMorphAttributes[ name ] = new BufferAttribute(
new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ),
morphAttr.itemSize,
morphAttr.normalized
);
}
}
// convert the error tolerance to an amount of decimal places to truncate to
const halfTolerance = tolerance * 0.5;
const exponent = Math.log10( 1 / tolerance );
const hashMultiplier = Math.pow( 10, exponent );
const hashAdditive = halfTolerance * hashMultiplier;
for ( let i = 0; i < vertexCount; i ++ ) {
const index = indices ? indices.getX( i ) : i;
// Generate a hash for the vertex attributes at the current index 'i'
let hash = '';
for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
const name = attributeNames[ j ];
const attribute = geometry.getAttribute( name );
const itemSize = attribute.itemSize;
for ( let k = 0; k < itemSize; k ++ ) {
// double tilde truncates the decimal value
hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`;
}
}
// Add another reference to the vertex if it's already
// used by another index
if ( hash in hashToIndex ) {
newIndices.push( hashToIndex[ hash ] );
} else {
// copy data to the new index in the temporary attributes
for ( let j = 0, l = attributeNames.length; j < l; j ++ ) {
const name = attributeNames[ j ];
const attribute = geometry.getAttribute( name );
const morphAttr = geometry.morphAttributes[ name ];
const itemSize = attribute.itemSize;
const newarray = tmpAttributes[ name ];
const newMorphArrays = tmpMorphAttributes[ name ];
for ( let k = 0; k < itemSize; k ++ ) {
const getterFunc = getters[ k ];
const setterFunc = setters[ k ];
newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) );
if ( morphAttr ) {
for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) {
newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) );
}
}
}
}
hashToIndex[ hash ] = nextIndex;
newIndices.push( nextIndex );
nextIndex ++;
}
}
// generate result BufferGeometry
const result = geometry.clone();
for ( const name in geometry.attributes ) {
const tmpAttribute = tmpAttributes[ name ];
result.setAttribute( name, new BufferAttribute(
tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ),
tmpAttribute.itemSize,
tmpAttribute.normalized,
) );
if ( ! ( name in tmpMorphAttributes ) ) continue;
for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) {
const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ];
result.morphAttributes[ name ][ j ] = new BufferAttribute(
tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ),
tmpMorphAttribute.itemSize,
tmpMorphAttribute.normalized,
);
}
}
// indices
result.setIndex( newIndices );
return result;
}
/**
* @param {BufferGeometry} geometry
* @param {number} drawMode
* @return {BufferGeometry}
*/
function toTrianglesDrawMode( geometry, drawMode ) {
if ( drawMode === TrianglesDrawMode ) {
console.warn( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.' );
return geometry;
}
if ( drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode ) {
let index = geometry.getIndex();
// generate index if not present
if ( index === null ) {
const indices = [];
const position = geometry.getAttribute( 'position' );
if ( position !== undefined ) {
for ( let i = 0; i < position.count; i ++ ) {
indices.push( i );
}
geometry.setIndex( indices );
index = geometry.getIndex();
} else {
console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );
return geometry;
}
}
//
const numberOfTriangles = index.count - 2;
const newIndices = [];
if ( drawMode === TriangleFanDrawMode ) {
// gl.TRIANGLE_FAN
for ( let i = 1; i <= numberOfTriangles; i ++ ) {
newIndices.push( index.getX( 0 ) );
newIndices.push( index.getX( i ) );
newIndices.push( index.getX( i + 1 ) );
}
} else {
// gl.TRIANGLE_STRIP
for ( let i = 0; i < numberOfTriangles; i ++ ) {
if ( i % 2 === 0 ) {
newIndices.push( index.getX( i ) );
newIndices.push( index.getX( i + 1 ) );
newIndices.push( index.getX( i + 2 ) );
} else {
newIndices.push( index.getX( i + 2 ) );
newIndices.push( index.getX( i + 1 ) );
newIndices.push( index.getX( i ) );
}
}
}
if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {
console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );
}
// build final geometry
const newGeometry = geometry.clone();
newGeometry.setIndex( newIndices );
newGeometry.clearGroups();
return newGeometry;
} else {
console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode );
return geometry;
}
}
/**
* Calculates the morphed attributes of a morphed/skinned BufferGeometry.
* Helpful for Raytracing or Decals.
* @param {Mesh | Line | Points} object An instance of Mesh, Line or Points.
* @return {Object} An Object with original position/normal attributes and morphed ones.
*/
function computeMorphedAttributes( object ) {
const _vA = new Vector3();
const _vB = new Vector3();
const _vC = new Vector3();
const _tempA = new Vector3();
const _tempB = new Vector3();
const _tempC = new Vector3();
const _morphA = new Vector3();
const _morphB = new Vector3();
const _morphC = new Vector3();
function _calculateMorphedAttributeData(
object,
attribute,
morphAttribute,
morphTargetsRelative,
a,
b,
c,
modifiedAttributeArray
) {
_vA.fromBufferAttribute( attribute, a );
_vB.fromBufferAttribute( attribute, b );
_vC.fromBufferAttribute( attribute, c );
const morphInfluences = object.morphTargetInfluences;
if ( morphAttribute && morphInfluences ) {
_morphA.set( 0, 0, 0 );
_morphB.set( 0, 0, 0 );
_morphC.set( 0, 0, 0 );
for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {
const influence = morphInfluences[ i ];
const morph = morphAttribute[ i ];
if ( influence === 0 ) continue;
_tempA.fromBufferAttribute( morph, a );
_tempB.fromBufferAttribute( morph, b );
_tempC.fromBufferAttribute( morph, c );
if ( morphTargetsRelative ) {
_morphA.addScaledVector( _tempA, influence );
_morphB.addScaledVector( _tempB, influence );
_morphC.addScaledVector( _tempC, influence );
} else {
_morphA.addScaledVector( _tempA.sub( _vA ), influence );
_morphB.addScaledVector( _tempB.sub( _vB ), influence );
_morphC.addScaledVector( _tempC.sub( _vC ), influence );
}
}
_vA.add( _morphA );
_vB.add( _morphB );
_vC.add( _morphC );
}
if ( object.isSkinnedMesh ) {
object.applyBoneTransform( a, _vA );
object.applyBoneTransform( b, _vB );
object.applyBoneTransform( c, _vC );
}
modifiedAttributeArray[ a * 3 + 0 ] = _vA.x;
modifiedAttributeArray[ a * 3 + 1 ] = _vA.y;
modifiedAttributeArray[ a * 3 + 2 ] = _vA.z;
modifiedAttributeArray[ b * 3 + 0 ] = _vB.x;
modifiedAttributeArray[ b * 3 + 1 ] = _vB.y;
modifiedAttributeArray[ b * 3 + 2 ] = _vB.z;
modifiedAttributeArray[ c * 3 + 0 ] = _vC.x;
modifiedAttributeArray[ c * 3 + 1 ] = _vC.y;
modifiedAttributeArray[ c * 3 + 2 ] = _vC.z;
}
const geometry = object.geometry;
const material = object.material;
let a, b, c;
const index = geometry.index;
const positionAttribute = geometry.attributes.position;
const morphPosition = geometry.morphAttributes.position;
const morphTargetsRelative = geometry.morphTargetsRelative;
const normalAttribute = geometry.attributes.normal;
const morphNormal = geometry.morphAttributes.position;
const groups = geometry.groups;
const drawRange = geometry.drawRange;
let i, j, il, jl;
let group;
let start, end;
const modifiedPosition = new Float32Array( positionAttribute.count * positionAttribute.itemSize );
const modifiedNormal = new Float32Array( normalAttribute.count * normalAttribute.itemSize );
if ( index !== null ) {
// indexed buffer geometry
if ( Array.isArray( material ) ) {
for ( i = 0, il = groups.length; i < il; i ++ ) {
group = groups[ i ];
start = Math.max( group.start, drawRange.start );
end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );
for ( j = start, jl = end; j < jl; j += 3 ) {
a = index.getX( j );
b = index.getX( j + 1 );
c = index.getX( j + 2 );
_calculateMorphedAttributeData(
object,
positionAttribute,
morphPosition,
morphTargetsRelative,
a, b, c,
modifiedPosition
);
_calculateMorphedAttributeData(
object,
normalAttribute,
morphNormal,
morphTargetsRelative,
a, b, c,
modifiedNormal
);
}
}
} else {
start = Math.max( 0, drawRange.start );
end = Math.min( index.count, ( drawRange.start + drawRange.count ) );
for ( i = start, il = end; i < il; i += 3 ) {
a = index.getX( i );
b = index.getX( i + 1 );
c = index.getX( i + 2 );
_calculateMorphedAttributeData(
object,
positionAttribute,
morphPosition,
morphTargetsRelative,
a, b, c,
modifiedPosition
);
_calculateMorphedAttributeData(
object,
normalAttribute,
morphNormal,
morphTargetsRelative,
a, b, c,
modifiedNormal
);
}
}
} else {
// non-indexed buffer geometry
if ( Array.isArray( material ) ) {
for ( i = 0, il = groups.length; i < il; i ++ ) {
group = groups[ i ];
start = Math.max( group.start, drawRange.start );
end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );
for ( j = start, jl = end; j < jl; j += 3 ) {
a = j;
b = j + 1;
c = j + 2;
_calculateMorphedAttributeData(
object,
positionAttribute,
morphPosition,
morphTargetsRelative,
a, b, c,
modifiedPosition
);
_calculateMorphedAttributeData(
object,
normalAttribute,
morphNormal,
morphTargetsRelative,
a, b, c,
modifiedNormal
);
}
}
} else {
start = Math.max( 0, drawRange.start );
end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );
for ( i = start, il = end; i < il; i += 3 ) {
a = i;
b = i + 1;
c = i + 2;
_calculateMorphedAttributeData(
object,
positionAttribute,
morphPosition,
morphTargetsRelative,
a, b, c,
modifiedPosition
);
_calculateMorphedAttributeData(
object,
normalAttribute,
morphNormal,
morphTargetsRelative,
a, b, c,
modifiedNormal
);
}
}
}
const morphedPositionAttribute = new Float32BufferAttribute( modifiedPosition, 3 );
const morphedNormalAttribute = new Float32BufferAttribute( modifiedNormal, 3 );
return {
positionAttribute: positionAttribute,
normalAttribute: normalAttribute,
morphedPositionAttribute: morphedPositionAttribute,
morphedNormalAttribute: morphedNormalAttribute
};
}
function mergeGroups( geometry ) {
if ( geometry.groups.length === 0 ) {
console.warn( 'THREE.BufferGeometryUtils.mergeGroups(): No groups are defined. Nothing to merge.' );
return geometry;
}
let groups = geometry.groups;
// sort groups by material index
groups = groups.sort( ( a, b ) => {
if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex;
return a.start - b.start;
} );
// create index for non-indexed geometries
if ( geometry.getIndex() === null ) {
const positionAttribute = geometry.getAttribute( 'position' );
const indices = [];
for ( let i = 0; i < positionAttribute.count; i += 3 ) {
indices.push( i, i + 1, i + 2 );
}
geometry.setIndex( indices );
}
// sort index
const index = geometry.getIndex();
const newIndices = [];
for ( let i = 0; i < groups.length; i ++ ) {
const group = groups[ i ];
const groupStart = group.start;
const groupLength = groupStart + group.count;
for ( let j = groupStart; j < groupLength; j ++ ) {
newIndices.push( index.getX( j ) );
}
}
geometry.dispose(); // Required to force buffer recreation
geometry.setIndex( newIndices );
// update groups indices
let start = 0;
for ( let i = 0; i < groups.length; i ++ ) {
const group = groups[ i ];
group.start = start;
start += group.count;
}
// merge groups
let currentGroup = groups[ 0 ];
geometry.groups = [ currentGroup ];
for ( let i = 1; i < groups.length; i ++ ) {
const group = groups[ i ];
if ( currentGroup.materialIndex === group.materialIndex ) {
currentGroup.count += group.count;
} else {
currentGroup = group;
geometry.groups.push( currentGroup );
}
}
return geometry;
}
/**
* Modifies the supplied geometry if it is non-indexed, otherwise creates a new,
* non-indexed geometry. Returns the geometry with smooth normals everywhere except
* faces that meet at an angle greater than the crease angle.
*
* @param {BufferGeometry} geometry
* @param {number} [creaseAngle]
* @return {BufferGeometry}
*/
function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) {
const creaseDot = Math.cos( creaseAngle );
const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
// reusable vectors
const verts = [ new Vector3(), new Vector3(), new Vector3() ];
const tempVec1 = new Vector3();
const tempVec2 = new Vector3();
const tempNorm = new Vector3();
const tempNorm2 = new Vector3();
// hashes a vector
function hashVertex( v ) {
const x = ~ ~ ( v.x * hashMultiplier );
const y = ~ ~ ( v.y * hashMultiplier );
const z = ~ ~ ( v.z * hashMultiplier );
return `${x},${y},${z}`;
}
// BufferGeometry.toNonIndexed() warns if the geometry is non-indexed
// and returns the original geometry
const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry;
const posAttr = resultGeometry.attributes.position;
const vertexMap = {};
// find all the normals shared by commonly located vertices
for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
const i3 = 3 * i;
const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
tempVec1.subVectors( c, b );
tempVec2.subVectors( a, b );
// add the normal to the map for all vertices
const normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize();
for ( let n = 0; n < 3; n ++ ) {
const vert = verts[ n ];
const hash = hashVertex( vert );
if ( ! ( hash in vertexMap ) ) {
vertexMap[ hash ] = [];
}
vertexMap[ hash ].push( normal );
}
}
// average normals from all vertices that share a common location if they are within the
// provided crease threshold
const normalArray = new Float32Array( posAttr.count * 3 );
const normAttr = new BufferAttribute( normalArray, 3, false );
for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) {
// get the face normal for this vertex
const i3 = 3 * i;
const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 );
const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 );
const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 );
tempVec1.subVectors( c, b );
tempVec2.subVectors( a, b );
tempNorm.crossVectors( tempVec1, tempVec2 ).normalize();
// average all normals that meet the threshold and set the normal value
for ( let n = 0; n < 3; n ++ ) {
const vert = verts[ n ];
const hash = hashVertex( vert );
const otherNormals = vertexMap[ hash ];
tempNorm2.set( 0, 0, 0 );
for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) {
const otherNorm = otherNormals[ k ];
if ( tempNorm.dot( otherNorm ) > creaseDot ) {
tempNorm2.add( otherNorm );
}
}
tempNorm2.normalize();
normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z );
}
}
resultGeometry.setAttribute( 'normal', normAttr );
return resultGeometry;
}
/*
export {
computeMikkTSpaceTangents,
mergeGeometries,
mergeAttributes,
interleaveAttributes,
estimateBytesUsed,
mergeVertices,
toTrianglesDrawMode,
computeMorphedAttributes,
mergeGroups,
toCreasedNormals
};*/
//import { mergeGeometries } from './BufferGeometryUtils.js';
class LDrawUtils {
static mergeObject( object ) {
// Merges geometries in object by materials and returns new object. Use on not indexed geometries.
// The object buffers reference the old object ones.
// Special treatment is done to the conditional lines generated by LDrawLoader.
function extractGroup( geometry, group, elementSize, isConditionalLine ) {
// Extracts a group from a geometry as a new geometry (with attribute buffers referencing original buffers)
const newGeometry = new BufferGeometry();
const originalPositions = geometry.getAttribute( 'position' ).array;
const originalNormals = elementSize === 3 ? geometry.getAttribute( 'normal' ).array : null;
const numVertsGroup = Math.min( group.count, Math.floor( originalPositions.length / 3 ) - group.start );
const vertStart = group.start * 3;
const vertEnd = ( group.start + numVertsGroup ) * 3;
const positions = originalPositions.subarray( vertStart, vertEnd );
const normals = originalNormals !== null ? originalNormals.subarray( vertStart, vertEnd ) : null;
newGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
if ( normals !== null ) newGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
if ( isConditionalLine ) {
const controlArray0 = geometry.getAttribute( 'control0' ).array.subarray( vertStart, vertEnd );
const controlArray1 = geometry.getAttribute( 'control1' ).array.subarray( vertStart, vertEnd );
const directionArray = geometry.getAttribute( 'direction' ).array.subarray( vertStart, vertEnd );
newGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );
newGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );
newGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );
}
return newGeometry;
}
function addGeometry( mat, geometry, geometries ) {
const geoms = geometries[ mat.uuid ];
if ( ! geoms ) {
geometries[ mat.uuid ] = {
mat: mat,
arr: [ geometry ]
};
} else {
geoms.arr.push( geometry );
}
}
function permuteAttribute( attribute, elemSize ) {
// Permutes first two vertices of each attribute element
if ( ! attribute ) return;
const verts = attribute.array;
const numVerts = Math.floor( verts.length / 3 );
let offset = 0;
for ( let i = 0; i < numVerts; i ++ ) {
const x = verts[ offset ];
const y = verts[ offset + 1 ];
const z = verts[ offset + 2 ];
verts[ offset ] = verts[ offset + 3 ];
verts[ offset + 1 ] = verts[ offset + 4 ];
verts[ offset + 2 ] = verts[ offset + 5 ];
verts[ offset + 3 ] = x;
verts[ offset + 4 ] = y;
verts[ offset + 5 ] = z;
offset += elemSize * 3;
}
}
// Traverse the object hierarchy collecting geometries and transforming them to world space
const meshGeometries = {};
const linesGeometries = {};
const condLinesGeometries = {};
object.updateMatrixWorld( true );
const normalMatrix = new Matrix3();
object.traverse( c => {
if ( c.isMesh | c.isLineSegments ) {
const elemSize = c.isMesh ? 3 : 2;
const geometry = c.geometry.clone();
const matrixIsInverted = c.matrixWorld.determinant() < 0;
if ( matrixIsInverted ) {
permuteAttribute( geometry.attributes.position, elemSize );
permuteAttribute( geometry.attributes.normal, elemSize );
}
geometry.applyMatrix4( c.matrixWorld );
if ( c.isConditionalLine ) {
geometry.attributes.control0.applyMatrix4( c.matrixWorld );
geometry.attributes.control1.applyMatrix4( c.matrixWorld );
normalMatrix.getNormalMatrix( c.matrixWorld );
geometry.attributes.direction.applyNormalMatrix( normalMatrix );
}
const geometries = c.isMesh ? meshGeometries : ( c.isConditionalLine ? condLinesGeometries : linesGeometries );
if ( Array.isArray( c.material ) ) {
for ( const groupIndex in geometry.groups ) {
const group = geometry.groups[ groupIndex ];
const mat = c.material[ group.materialIndex ];
const newGeometry = extractGroup( geometry, group, elemSize, c.isConditionalLine );
addGeometry( mat, newGeometry, geometries );
}
} else {
addGeometry( c.material, geometry, geometries );
}
}
} );
// Create object with merged geometries
const mergedObject = new Group();
const meshMaterialsIds = Object.keys( meshGeometries );
for ( const meshMaterialsId of meshMaterialsIds ) {
const meshGeometry = meshGeometries[ meshMaterialsId ];
const mergedGeometry = mergeGeometries( meshGeometry.arr );
mergedObject.add( new Mesh( mergedGeometry, meshGeometry.mat ) );
}
const linesMaterialsIds = Object.keys( linesGeometries );
for ( const linesMaterialsId of linesMaterialsIds ) {
const lineGeometry = linesGeometries[ linesMaterialsId ];
const mergedGeometry = mergeGeometries( lineGeometry.arr );
mergedObject.add( new LineSegments( mergedGeometry, lineGeometry.mat ) );
}
const condLinesMaterialsIds = Object.keys( condLinesGeometries );
for ( const condLinesMaterialsId of condLinesMaterialsIds ) {
const condLineGeometry = condLinesGeometries[ condLinesMaterialsId ];
const mergedGeometry = mergeGeometries( condLineGeometry.arr );
const condLines = new LineSegments( mergedGeometry, condLineGeometry.mat );
condLines.isConditionalLine = true;
mergedObject.add( condLines );
}
mergedObject.userData.constructionStep = 0;
mergedObject.userData.numConstructionSteps = 1;
return mergedObject;
}
}
//export { LDrawUtils };
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
// somewhere in the Loop class:
this.updatables = []
}
start() {
this.renderer.setAnimationLoop(() => {
// tell every animated object to tick forward one frame
// this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick(){
// only call the getDelta function once per frame!
const delta = clock.getDelta();
// console.log(
// `The last frame rendered in ${delta * 1000} milliseconds`,
// );
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if(this.updatables.length){
for (const object of this.updatables) {
if(typeof object.tick == 'function'){
object.tick(delta);
}
}
}
}
}
//export { Loop };
执行代码
现在我们已经成功添加了很多功能和复杂的交互逻辑,将不同的细节进行分层管理。后续可采用 MVC 模式重构代码,将代码分为三个层级:模型层、视图层和控制层。模型层负责数据的管理,视图层负责展示数据和渲染 UI,控制层则负责协调模型层和视图层之间的交互,同时处理一些业务逻辑。重构后代码层级会更清晰,方便拓展其功能。
最后,将脚本执行到dom即可看到模型。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta name="force-rendering" content="webkit">
<meta name="google-site-verification" content="FTeR0c8arOPKh8c5DYh_9uu98_zJbaWw53J-Sch9MTg">
<meta data-rh="true" name="keywords" content="three.js实现乐高小轿车">
<meta data-rh="true" name="description" content="three.js实现乐高小轿车">
<meta data-rh="true" property="og:title" content="three.js实现乐高小轿车">
<link rel="icon" href="./favicon.ico">
<title>three.js实现乐高小轿车</title>
<style>
body {
padding: 0;
margin: 0;
font: normal 14px/1.42857 Tahoma;
}
#scene-container {
height: 100vh;
}
</style>
</head>
<body onload="initViewer()">
<div id="scene-container"></div>
<script>
function initViewer(){
container = document.querySelector('#scene-container');
let ldraw = new Ldraw();
ldraw.start();
}
</script>
</body>
</html>
模型描述文本
0 LDraw.org Configuration File
0 Name: LDConfig.ldr
0 Author: LDraw.org
0 !LDRAW_ORG Configuration UPDATE 2017-12-15
0 // LDraw Solid Colours
0 // LEGOID 26 - Black
0 !COLOUR Black CODE 0 VALUE #05131D EDGE #595959
0 // LEGOID 23 - Bright Blue
0 !COLOUR Blue CODE 1 VALUE #0055BF EDGE #333333
0 // LEGOID 28 - Dark Green
0 !COLOUR Green CODE 2 VALUE #257A3E EDGE #333333
0 // LEGOID 107 - Bright Bluish Green
0 !COLOUR Dark_Turquoise CODE 3 VALUE #00838F EDGE #333333
0 // LEGOID 21 - Bright Red
0 !COLOUR Red CODE 4 VALUE #C91A09 EDGE #333333
0 // LEGOID 221 - Bright Purple
0 !COLOUR Dark_Pink CODE 5 VALUE #C870A0 EDGE #333333
0 // LEGOID 217 - Brown
0 !COLOUR Brown CODE 6 VALUE #583927 EDGE #1E1E1E
0 // LEGOID 2 - Grey
0 !COLOUR Light_Grey CODE 7 VALUE #9BA19D EDGE #333333
0 // LEGOID 27 - Dark Grey
0 !COLOUR Dark_Grey CODE 8 VALUE #6D6E5C EDGE #333333
0 // LEGOID 45 - Light Blue
0 !COLOUR Light_Blue CODE 9 VALUE #B4D2E3 EDGE #333333
0 // LEGOID 37 - Bright Green
0 !COLOUR Bright_Green CODE 10 VALUE #4B9F4A EDGE #333333
0 // LEGOID 116 - Medium Bluish Green
0 !COLOUR Light_Turquoise CODE 11 VALUE #55A5AF EDGE #333333
0 // LEGOID 4 - Brick Red
0 !COLOUR Salmon CODE 12 VALUE #F2705E EDGE #333333
0 // LEGOID 9 - Light Reddish Violet
0 !COLOUR Pink CODE 13 VALUE #FC97AC EDGE #333333
0 // LEGOID 24 - Bright Yellow
0 !COLOUR Yellow CODE 14 VALUE #F2CD37 EDGE #333333
还原模型到三维场景
参见: