本文主要是介绍分享three.js实现乐高小汽车,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
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方式来编写代码。
实践
容器
首先,准备一个空白容器,让它的尺寸与浏览器视窗大小相同,以充分利用屏幕空间。
<div id="scene-container"></div>
依赖
对于 JS 脚本,使用 导入映射 配置资源的 CDN 地址,这样就可以像使用 npm 包一样导入相关资源。
<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>
接着就可以引入依赖。
<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>
设计变量、类、方法
定义相关变量
let container, progressBarDiv;
let camera, scene, renderer, controls, gui, guiData, anLoop;
let model;
const modelFileList = {'Car': './car.txt'}
设计乐高类
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 );// rendererrenderer = new THREE.WebGLRenderer( { antialias: true } );//renderer.setSize( window.innerWidth, window.innerHeight );renderer.setSize(container.clientWidth, container.clientHeight);// eslint-disable-next-line no-undefrenderer.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 );// sceneconst 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);// guiguiData = {//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 modelthis.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 settingc.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 timeconst 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 lookif ( 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 materialif ( guiData.mergeModel ) model = LDrawUtils.mergeObject( model );// Convert from LDraw coordinates: rotate 180 degrees around OXmodel.rotation.x = Math.PI;scene.add( model );guiData.buildingStep = model.userData.numBuildingSteps - 1;this.updateObjectsVisibility;// Adjust camera and lightconst 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 hereanLoop.tick();}}//export { Ldraw }
创建一个场景(Scene)、一个透视相机(PerspectiveCamera)和一个 WebGL 渲染器(WebGLRenderer),并将渲染器添加到 DOM 中。同时,编写一个渲染函数,使用requestAnimationFrame
方法循环渲染场景。
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 moveconst _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 controlthis.enabled = true;// "target" sets the location of focus, where the object orbits aroundthis.target = new Vector3();// Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effectthis.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 cursorthis.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; // radiansthis.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; // radiansthis.maxAzimuthAngle = Infinity; // radians// Set to true to enable damping (inertia)// If damping is enabled, you must call controls.update() in your animation loopthis.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 zoomingthis.enableZoom = true;this.zoomSpeed = 1.0;// Set to false to disable rotatingthis.enableRotate = true;this.rotateSpeed = 1.0;// Set to false to disable panningthis.enablePan = true;this.panSpeed = 1.0;this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.upthis.keyPanSpeed = 7.0; // pixels moved per arrow key pushthis.zoomToCursor = false;// Set to true to automatically rotate around the target// If auto-rotate is enabled, you must call controls.update() in your animation loopthis.autoRotate = false;this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60// The four arrow keysthis.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };// Mouse buttonsthis.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };// Touch fingersthis.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };// for resetthis.target0 = this.target.clone();this.position0 = this.object.position.clone();this.zoom0 = this.object.zoom;// the target DOM element for key eventsthis._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 axisconst 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" spaceoffset.applyQuaternion( quat );// angle from z-axis around y-axisspherical.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 limitslet 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 limitsspherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );spherical.makeSafe();// move target to panned locationif ( 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 interestscope.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 casesif ( 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" spaceoffset.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 positionif ( scope.zoomToCursor && performCursorZoom ) {let newRadius = null;if ( scope.object.isPerspectiveCamera ) {// move the camera down the pointer ray// this method avoids floating point errorconst 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 changesconst 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 targetif ( newRadius !== null ) {if ( this.screenSpacePanning ) {// position the orbit target in front of the new camera positionscope.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 valuesif ( 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 / 8if ( 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 compatibilitydocument.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 coordinatesconst 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 objectMatrixv.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 positiveconst pan = function () {const offset = new Vector3();return function pan( deltaX, deltaY ) {const element = scope.domElement;if ( scope.object.isPerspectiveCamera ) {// perspectiveconst position = scope.object.position;offset.copy( position ).sub( scope.target );let targetDistance = offset.length();// half of the fov is center to top of screentargetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );// we use only clientHeight here so aspect ratio does not distort speedpanLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );} else if ( scope.object.isOrthographicCamera ) {// orthographicpanLeft( 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 perspectiveconsole.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, heightrotateUp( 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 keysevent.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, heightrotateUp( 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-uponTouchStart( { 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 demandconst newEvent = {clientX: event.clientX,clientY: event.clientY,deltaY: event.deltaY,};switch ( mode ) {case 1: // LINE_MODEnewEvent.deltaY *= 16;break;case 2: // PAGE_MODEnewEvent.deltaY *= 100;break;}// detect if event was triggered by pinchingif ( event.ctrlKey && ! controlActive ) {newEvent.deltaY *= 10;}return newEvent;}function interceptControlDown( event ) {if ( event.key === 'Control' ) {controlActive = true;const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } );}}function interceptControlUp( event ) {if ( event.key === 'Control' ) {controlActive = false;const document = scope.domElement.getRootNode(); // offscreen canvas compatibilitydocument.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 compatibilitydocument.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } );// force an update at startthis.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 rightconst 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 leftconst 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 );// +xconst 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 );// +zconst 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 );// -zconst 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 );// +yconst 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 implementedconst 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 spacevec4 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 vectorvec2 dir = p1.xy - p0.xy;vec2 norm = vec2( -dir.y, dir.x );// Get control point directions from the linevec2 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 shadergl_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 hashfor ( 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 mapconst 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 trianglefor ( 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 hardif ( 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 boundsif ( 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" normalslet 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 normalswhile ( true ) {// Stop if there are no more faces leftlet halfEdge = null;for ( const key in halfEdgeList ) {halfEdge = halfEdgeList[ key ];break;}if ( halfEdge === null ) {break;}// Exhaustively find all connected facesconst queue = [ halfEdge ];while ( queue.length > 0 ) {// initialize all vertex normals in this triangleconst 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 edgeconst 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 againconst 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 normalconst 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 normalif (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 spacewhile ( 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 modifiedresult.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 pathlocationState = FILE_LOCATION_NOT_FOUND;} else {// Next attempt is lower casefileName = 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 resultsconst 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 linesif ( text.indexOf( '\r\n' ) !== - 1 ) {// This is faster than String.split with regex that splits on bothtext = 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 commandsfor ( 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 cachethis.setData( currentEmbeddedFileName, currentEmbeddedText );// New embedded text filecurrentEmbeddedFileName = line.substring( 7 );currentEmbeddedText = '';} else {currentEmbeddedText += line + '\n';}continue;}const lp = new LineParser( line, lineIndex + 1 );lp.seekNonSpace();if ( lp.isAtTheEnd() ) {// Empty linecontinue;}// Parse the line typeconst 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 METAcase '0':// Parse meta directiveconst 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 parsingparsingEmbeddedFiles = true;currentEmbeddedFileName = lp.getRemainingString();currentEmbeddedText = '';bfcCertified = false;bfcCCW = true;}break;case 'BFC':// Changes to the backface culling statewhile ( ! 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 implementedbreak;}}break;// Line type 1: Sub-object filecase '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 mapfileName = loader.fileMap[ fileName ];} else {// Standardized subfoldersif ( 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 segmentcase '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 segmentcase '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: Trianglecase '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: Quadrilateralcase '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 datagetData( 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 processingthis._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 datasetData( 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 loadcontinue;}// if the subobject was loaded as a separate group then apply the parent scopes materialsif ( 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 primitivesif ( 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 copyconst 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 groupselements.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 facesif ( 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 providedlet 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.Materialthis.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 materialsthis.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 edgesthis.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 arrayconst 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 libconst 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" colorsmaterial = loader.getMaterial( colorCode );if ( material === null ) {// otherwise throw a warning if this is final opportunity to set the materialconsole.warn( `LDrawLoader: Material properties for code ${ colorCode } not available.` );// And return the 'missing color' materialmaterial = 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.Materiallet code = null;// Triangle and line colorslet fillColor = '#FF00FF';let edgeColor = '#FF00FF';// Transparencylet 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 parameterslet 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 codeedgeMaterial = this.getMaterial( edgeColor );if ( ! edgeMaterial ) {throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' );}// Get the edge material for this triangle materialedgeMaterial = 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 implementedlineParser.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 glossymaterial = new MeshStandardMaterial( { roughness: 0.3, metalness: 0.25 } );break;case FINISH_TYPE_CHROME:// Mirror finish surfacematerial = new MeshStandardMaterial( { roughness: 0, metalness: 1 } );break;case FINISH_TYPE_RUBBER:// Rubber finishmaterial = new MeshStandardMaterial( { roughness: 0.9, metalness: 0 } );break;case FINISH_TYPE_MATTE_METALLIC:// Brushed metal finishmaterial = new MeshStandardMaterial( { roughness: 0.8, metalness: 0.4 } );break;case FINISH_TYPE_METAL:// Average metal finishmaterial = new MeshStandardMaterial( { roughness: 0.2, metalness: 0.85 } );break;default:// Should not happenbreak;}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 edgesedgeMaterial = 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 edgesconst 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 successlet 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 awayif ( 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 WebGLBackgroundvirtualCamera.updateMatrixWorld();virtualCamera.projectionMatrix.copy( camera.projectionMatrix );// Update the texture matrixtextureMatrix.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.pdfreflectorPlane.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 vectorclipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) );// Replacing the third row of the projection matrixprojectionMatrix.elements[ 2 ] = clipPlane.x;projectionMatrix.elements[ 6 ] = clipPlane.y;projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias;projectionMatrix.elements[ 14 ] = clipPlane.w;// Renderscope.visible = false;const currentRenderTarget = renderer.getRenderTarget();const currentXrEnabled = renderer.xr.enabled;const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;renderer.xr.enabled = false; // Avoid camera modificationrenderer.shadowMap.autoUpdate = false; // Avoid re-computing shadowsrenderer.setRenderTarget( renderTarget );renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897if ( renderer.autoClear === false ) renderer.clear();renderer.render( scene, virtualCamera );renderer.xr.enabled = currentXrEnabled;renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;renderer.setRenderTarget( currentRenderTarget );// Restore viewportconst 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 noneif ( 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 differentfor ( 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 attributesif ( 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 differentif ( 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 indicesif ( 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 attributesfor ( 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 attributesfor ( 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 attributelet TypedArray;let arrayLength = 0;let stride = 0;// calculate the length and type of the interleavedBufferfor ( 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 attributesconst 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 offsetfor ( 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 attributeexport 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 geometryexport 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 indiceslet nextIndex = 0;// attributes and new attribute arraysconst 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 toconst 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 valuehash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`;}}// Add another reference to the vertex if it's already// used by another indexif ( hash in hashToIndex ) {newIndices.push( hashToIndex[ hash ] );} else {// copy data to the new index in the temporary attributesfor ( 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 BufferGeometryconst 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,);}}// indicesresult.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 presentif ( 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_FANfor ( 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_STRIPfor ( 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 geometryconst 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 geometryif ( 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 geometryif ( 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 indexgroups = groups.sort( ( a, b ) => {if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex;return a.start - b.start;} );// create index for non-indexed geometriesif ( 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 indexconst 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 recreationgeometry.setIndex( newIndices );// update groups indiceslet start = 0;for ( let i = 0; i < groups.length; i ++ ) {const group = groups[ i ];group.start = start;start += group.count;}// merge groupslet 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 vectorsconst 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 vectorfunction 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 geometryconst resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry;const posAttr = resultGeometry.attributes.position;const vertexMap = {};// find all the normals shared by commonly located verticesfor ( 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 verticesconst 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 thresholdconst 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 vertexconst 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 valuefor ( 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 elementif ( ! 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 spaceconst 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 geometriesconst 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 framethis.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-expressionsif(this.updatables.length){for (const object of this.updatables) {if(typeof object.tick == 'function'){object.tick(delta);}}}}}//export { Loop };
执行代码
现在我们已经成功添加了很多功能和复杂的交互逻辑,将不同的细节进行分层管理。后续可采用 MVC 模式重构代码,将代码分为三个层级:模型层、视图层和控制层。模型层负责数据的管理,视图层负责展示数据和渲染 UI,控制层则负责协调模型层和视图层之间的交互,同时处理一些业务逻辑。重构后代码层级会更清晰,方便拓展其功能。
最后,将脚本执行到dom即可看到模型。
<!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-150 // 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
还原模型到三维场景
参见:
3. 开发和学习环境,引入threejs | Three.js中文网
LDraw.org - LDraw.org Homepage
这篇关于分享three.js实现乐高小汽车的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!