| New file |
| | |
| | | /* |
| | | * Eslint config file |
| | | * Documentation: https://eslint.org/docs/user-guide/configuring/ |
| | | * Install the Eslint extension before using this feature. |
| | | */ |
| | | module.exports = { |
| | | env: { |
| | | es6: true, |
| | | browser: true, |
| | | node: true, |
| | | }, |
| | | ecmaFeatures: { |
| | | modules: true, |
| | | }, |
| | | parserOptions: { |
| | | ecmaVersion: 2018, |
| | | sourceType: 'module', |
| | | }, |
| | | globals: { |
| | | wx: true, |
| | | App: true, |
| | | Page: true, |
| | | getCurrentPages: true, |
| | | getApp: true, |
| | | Component: true, |
| | | requirePlugin: true, |
| | | requireMiniProgram: true, |
| | | }, |
| | | // extends: 'eslint:recommended', |
| | | rules: {}, |
| | | } |
| New file |
| | |
| | | // app.js |
| | | App({ |
| | | onLaunch() { |
| | | // 展示本地存储能力 |
| | | const logs = wx.getStorageSync('logs') || [] |
| | | logs.unshift(Date.now()) |
| | | wx.setStorageSync('logs', logs) |
| | | |
| | | // 登录 |
| | | wx.login({ |
| | | success: res => { |
| | | // 发送 res.code 到后台换取 openId, sessionKey, unionId |
| | | } |
| | | }) |
| | | }, |
| | | globalData: { |
| | | userInfo: null |
| | | } |
| | | }) |
| New file |
| | |
| | | { |
| | | "pages": [ |
| | | "pages/index/index", |
| | | "pages/logs/logs", |
| | | "pages/ThreeModel/ThreeModel" |
| | | ], |
| | | "window": { |
| | | "backgroundTextStyle": "light", |
| | | "navigationBarBackgroundColor": "#fff", |
| | | "navigationBarTitleText": "Weixin", |
| | | "navigationBarTextStyle": "black" |
| | | }, |
| | | "style": "v2", |
| | | "sitemapLocation": "sitemap.json" |
| | | } |
| New file |
| | |
| | | /**app.wxss**/ |
| | | @import "./asstes/CSS/icon.wxss"; |
| | | .container { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 200rpx 0; |
| | | box-sizing: border-box; |
| | | } |
| New file |
| | |
| | | @font-face { |
| | | font-family: "iconfont"; /* Project id 2927301 */ |
| | | src: url('//at.alicdn.com/t/font_2927301_pb0nhfotata.woff2?t=1636528701480') format('woff2'), |
| | | url('//at.alicdn.com/t/font_2927301_pb0nhfotata.woff?t=1636528701480') format('woff'), |
| | | url('//at.alicdn.com/t/font_2927301_pb0nhfotata.ttf?t=1636528701480') format('truetype'); |
| | | } |
| | | |
| | | .iconfont { |
| | | font-family: "iconfont" !important; |
| | | font-size: 16px; |
| | | font-style: normal; |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | } |
| | | |
| | | .icon-guanbi:before { |
| | | content: "\e604"; |
| | | } |
| | | |
| | | .icon-bofang:before { |
| | | content: "\e87c"; |
| | | } |
| | | |
| | | .icon-yanzhengmashibie:before { |
| | | content: "\e658"; |
| | | } |
| | | |
| | | .icon-tubiaoji_tupianshibie:before { |
| | | content: "\e696"; |
| | | } |
| | | |
| | | .icon-xiangji:before { |
| | | content: "\e616"; |
| | | } |
| | | |
| | | .icon-view:before { |
| | | content: "\e607"; |
| | | } |
| New file |
| | |
| | | Component({ |
| | | properties: { |
| | | text: {type: String, value: null}, |
| | | }, |
| | | data: {}, |
| | | methods: {}, |
| | | }); |
| New file |
| | |
| | | { |
| | | "component": true, |
| | | "usingComponents": { |
| | | } |
| | | } |
| New file |
| | |
| | | <view class="container"> |
| | | <view class="line"></view> |
| | | <view class="loading"> |
| | | <view class="loading-ani"></view> |
| | | <view class="loading-ani"></view> |
| | | <view class="loading-ani"></view> |
| | | <view class="loading-ani"></view> |
| | | <view class="loading-ani"></view> |
| | | </view> |
| | | </view> |
| | | <view wx:if="{{text}}" class="text">{{text}}</view> |
| New file |
| | |
| | | .container { |
| | | width: 400rpx; |
| | | height: 360rpx; |
| | | position: relative; |
| | | } |
| | | |
| | | .line { |
| | | position: absolute; |
| | | width: 400rpx; |
| | | height: 12rpx; |
| | | background: linear-gradient(90deg, rgba(243, 152, 0, 0) 0%, rgba(243, 152, 0, 1) 20%, rgba(243, 152, 0, 1) 80%, rgba(243, 152, 0, 0) 100%); |
| | | top: 0; |
| | | bottom: 0; |
| | | margin: auto; |
| | | } |
| | | |
| | | .text { |
| | | text-align: center; |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #FFFFFF; |
| | | text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .loading { |
| | | position: absolute; |
| | | width: fit-content; |
| | | height: fit-content; |
| | | top: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | margin: auto; |
| | | } |
| | | |
| | | .loading-ani { |
| | | width: 12rpx; |
| | | height: 100rpx; |
| | | background: #f39800; |
| | | display: inline-block; |
| | | margin: 0 10rpx; |
| | | animation: loading-ani linear 1s infinite; |
| | | } |
| | | |
| | | .loading-ani:nth-child(1) { |
| | | animation-delay: 0s; |
| | | } |
| | | |
| | | .loading-ani:nth-child(2) { |
| | | animation-delay: -.15s; |
| | | } |
| | | |
| | | .loading-ani:nth-child(3) { |
| | | animation-delay: -.3s; |
| | | } |
| | | |
| | | .loading-ani:nth-child(4) { |
| | | animation-delay: -.45s; |
| | | } |
| | | |
| | | .loading-ani:nth-child(5) { |
| | | animation-delay: -.6s; |
| | | } |
| | | |
| | | @keyframes loading-ani { |
| | | 0%, 100%, 60% { |
| | | transform: scaleY(1) |
| | | } |
| | | |
| | | 30% { |
| | | transform: scaleY(3) |
| | | } |
| | | } |
| New file |
| | |
| | | |
| | | |
| | | ( function () { |
| | | |
| | | var CameraControls = function ( object, domElement ) { |
| | | |
| | | if ( domElement === undefined ) console.warn( 'THREE.CameraControls: The second parameter "domElement" is now mandatory.' ); |
| | | if ( domElement === document ) console.error( 'THREE.CameraControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); |
| | | this.object = object; |
| | | this.domElement = domElement; // Set to false to disable this control |
| | | |
| | | this.enabled = true; // "target" sets the location of focus, where the object orbits around |
| | | |
| | | this.target = new THREE.Vector3(); // Set to true to enable trackball behavior |
| | | |
| | | this.trackball = false; // 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; // How far you can orbit vertically, upper and lower limits. |
| | | // Range is 0 to Math.PI radians. |
| | | |
| | | this.minPolarAngle = 0; // radians |
| | | |
| | | this.maxPolarAngle = Math.PI; // radians |
| | | // How far you can orbit horizontally, upper and lower limits. |
| | | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. |
| | | |
| | | this.minAzimuthAngle = - Infinity; // radians |
| | | |
| | | this.maxAzimuthAngle = Infinity; // radians |
| | | // Set to true to enable damping (inertia) |
| | | // If damping is enabled, you must call controls.update() in your animation loop |
| | | |
| | | this.enableDamping = false; |
| | | this.dampingFactor = 0.05; // This option enables dollying in and out; property named as "zoom" for backwards compatibility |
| | | // Set to false to disable zooming |
| | | |
| | | this.enableZoom = true; |
| | | this.zoomSpeed = 1.0; // Set to false to disable rotating |
| | | |
| | | this.enableRotate = true; |
| | | this.rotateSpeed = 1.0; // Set to false to disable panning |
| | | |
| | | this.enablePan = true; |
| | | this.panSpeed = 1.0; |
| | | this.screenSpacePanning = false; // if true, pan in screen-space |
| | | |
| | | this.keyPanSpeed = 7.0; // pixels moved per arrow key push |
| | | // Set to true to automatically rotate around the target |
| | | // If auto-rotate is enabled, you must call controls.update() in your animation loop |
| | | // auto-rotate is not supported for trackball behavior |
| | | |
| | | this.autoRotate = false; |
| | | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 |
| | | // Set to false to disable use of the keys |
| | | |
| | | this.enableKeys = true; // The four arrow keys |
| | | |
| | | this.keys = { |
| | | LEFT: 37, |
| | | UP: 38, |
| | | RIGHT: 39, |
| | | BOTTOM: 40 |
| | | }; // Mouse buttons |
| | | |
| | | this.mouseButtons = { |
| | | LEFT: THREE.MOUSE.ROTATE, |
| | | MIDDLE: THREE.MOUSE.DOLLY, |
| | | RIGHT: THREE.MOUSE.PAN |
| | | }; // Touch fingers |
| | | |
| | | this.touches = { |
| | | ONE: THREE.TOUCH.ROTATE, |
| | | TWO: THREE.TOUCH.DOLLY_PAN |
| | | }; // for reset |
| | | |
| | | this.target0 = this.target.clone(); |
| | | this.position0 = this.object.position.clone(); |
| | | this.quaternion0 = this.object.quaternion.clone(); |
| | | this.zoom0 = this.object.zoom; // |
| | | // public methods |
| | | // |
| | | |
| | | this.getPolarAngle = function () { |
| | | |
| | | return spherical.phi; |
| | | |
| | | }; |
| | | |
| | | this.getAzimuthalAngle = function () { |
| | | |
| | | return spherical.theta; |
| | | |
| | | }; |
| | | |
| | | this.saveState = function () { |
| | | |
| | | scope.target0.copy( scope.target ); |
| | | scope.position0.copy( scope.object.position ); |
| | | scope.quaternion0.copy( scope.object.quaternion ); |
| | | scope.zoom0 = scope.object.zoom; |
| | | |
| | | }; |
| | | |
| | | this.reset = function () { |
| | | |
| | | scope.target.copy( scope.target0 ); |
| | | scope.object.position.copy( scope.position0 ); |
| | | scope.object.quaternion.copy( scope.quaternion0 ); |
| | | 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 () { |
| | | |
| | | var offset = new THREE.Vector3(); // so camera.up is the orbit axis |
| | | |
| | | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); |
| | | var quatInverse = quat.clone().invert(); |
| | | var lastPosition = new THREE.Vector3(); |
| | | var lastQuaternion = new THREE.Quaternion(); |
| | | var q = new THREE.Quaternion(); |
| | | var vec = new THREE.Vector3(); |
| | | return function update() { |
| | | |
| | | var position = scope.object.position; |
| | | offset.copy( position ).sub( scope.target ); |
| | | |
| | | if ( scope.trackball ) { |
| | | |
| | | // rotate around screen-space y-axis |
| | | if ( sphericalDelta.theta ) { |
| | | |
| | | vec.set( 0, 1, 0 ).applyQuaternion( scope.object.quaternion ); |
| | | var factor = scope.enableDamping ? scope.dampingFactor : 1; |
| | | q.setFromAxisAngle( vec, sphericalDelta.theta * factor ); |
| | | scope.object.quaternion.premultiply( q ); |
| | | offset.applyQuaternion( q ); |
| | | |
| | | } // rotate around screen-space x-axis |
| | | |
| | | |
| | | if ( sphericalDelta.phi ) { |
| | | |
| | | vec.set( 1, 0, 0 ).applyQuaternion( scope.object.quaternion ); |
| | | var factor = scope.enableDamping ? scope.dampingFactor : 1; |
| | | q.setFromAxisAngle( vec, sphericalDelta.phi * factor ); |
| | | scope.object.quaternion.premultiply( q ); |
| | | offset.applyQuaternion( q ); |
| | | |
| | | } |
| | | |
| | | offset.multiplyScalar( scale ); |
| | | offset.clampLength( scope.minDistance, scope.maxDistance ); |
| | | |
| | | } else { |
| | | |
| | | // rotate offset to "y-axis-is-up" space |
| | | offset.applyQuaternion( quat ); |
| | | |
| | | if ( scope.autoRotate && state === STATE.NONE ) { |
| | | |
| | | rotateLeft( getAutoRotationAngle() ); |
| | | |
| | | } |
| | | |
| | | spherical.setFromVector3( offset ); |
| | | |
| | | if ( scope.enableDamping ) { |
| | | |
| | | spherical.theta += sphericalDelta.theta * scope.dampingFactor; |
| | | spherical.phi += sphericalDelta.phi * scope.dampingFactor; |
| | | |
| | | } else { |
| | | |
| | | spherical.theta += sphericalDelta.theta; |
| | | spherical.phi += sphericalDelta.phi; |
| | | |
| | | } // restrict theta to be between desired limits |
| | | |
| | | |
| | | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); // restrict phi to be between desired limits |
| | | |
| | | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); |
| | | spherical.makeSafe(); |
| | | spherical.radius *= scale; // restrict radius to be between desired limits |
| | | |
| | | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); |
| | | offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space |
| | | |
| | | offset.applyQuaternion( quatInverse ); |
| | | |
| | | } // move target to panned location |
| | | |
| | | |
| | | if ( scope.enableDamping === true ) { |
| | | |
| | | scope.target.addScaledVector( panOffset, scope.dampingFactor ); |
| | | |
| | | } else { |
| | | |
| | | scope.target.add( panOffset ); |
| | | |
| | | } |
| | | |
| | | position.copy( scope.target ).add( offset ); |
| | | |
| | | if ( scope.trackball === false ) { |
| | | |
| | | 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 ); |
| | | |
| | | } |
| | | |
| | | scale = 1; // update condition is: |
| | | // min(camera displacement, camera rotation in radians)^2 > EPS |
| | | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 |
| | | |
| | | if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { |
| | | |
| | | scope.dispatchEvent( changeEvent ); |
| | | lastPosition.copy( scope.object.position ); |
| | | lastQuaternion.copy( scope.object.quaternion ); |
| | | zoomChanged = false; |
| | | return true; |
| | | |
| | | } |
| | | |
| | | return false; |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | this.dispose = function () { |
| | | |
| | | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); |
| | | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); |
| | | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); |
| | | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); |
| | | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); |
| | | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); |
| | | document.removeEventListener( 'mousemove', onMouseMove, false ); |
| | | document.removeEventListener( 'mouseup', onMouseUp, false ); |
| | | scope.domElement.removeEventListener( 'keydown', onKeyDown, false ); //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? |
| | | |
| | | }; // |
| | | // internals |
| | | // |
| | | |
| | | |
| | | var scope = this; |
| | | var changeEvent = { |
| | | type: 'change' |
| | | }; |
| | | var startEvent = { |
| | | type: 'start' |
| | | }; |
| | | var endEvent = { |
| | | type: 'end' |
| | | }; |
| | | var STATE = { |
| | | NONE: - 1, |
| | | ROTATE: 0, |
| | | DOLLY: 1, |
| | | PAN: 2, |
| | | TOUCH_ROTATE: 3, |
| | | TOUCH_PAN: 4, |
| | | TOUCH_DOLLY_PAN: 5, |
| | | TOUCH_DOLLY_ROTATE: 6 |
| | | }; |
| | | var state = STATE.NONE; |
| | | var EPS = 0.000001; // current position in spherical coordinates |
| | | |
| | | var spherical = new THREE.Spherical(); |
| | | var sphericalDelta = new THREE.Spherical(); |
| | | var scale = 1; |
| | | var panOffset = new THREE.Vector3(); |
| | | var zoomChanged = false; |
| | | var rotateStart = new THREE.Vector2(); |
| | | var rotateEnd = new THREE.Vector2(); |
| | | var rotateDelta = new THREE.Vector2(); |
| | | var panStart = new THREE.Vector2(); |
| | | var panEnd = new THREE.Vector2(); |
| | | var panDelta = new THREE.Vector2(); |
| | | var dollyStart = new THREE.Vector2(); |
| | | var dollyEnd = new THREE.Vector2(); |
| | | var dollyDelta = new THREE.Vector2(); |
| | | |
| | | function getAutoRotationAngle() { |
| | | |
| | | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; |
| | | |
| | | } |
| | | |
| | | function getZoomScale() { |
| | | |
| | | return Math.pow( 0.95, scope.zoomSpeed ); |
| | | |
| | | } |
| | | |
| | | function rotateLeft( angle ) { |
| | | |
| | | sphericalDelta.theta -= angle; |
| | | |
| | | } |
| | | |
| | | function rotateUp( angle ) { |
| | | |
| | | sphericalDelta.phi -= angle; |
| | | |
| | | } |
| | | |
| | | var panLeft = function () { |
| | | |
| | | var v = new THREE.Vector3(); |
| | | return function panLeft( distance, objectMatrix ) { |
| | | |
| | | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix |
| | | |
| | | v.multiplyScalar( - distance ); |
| | | panOffset.add( v ); |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | var panUp = function () { |
| | | |
| | | var v = new THREE.Vector3(); |
| | | return function panUp( distance, objectMatrix ) { |
| | | |
| | | if ( scope.screenSpacePanning === true ) { |
| | | |
| | | v.setFromMatrixColumn( objectMatrix, 1 ); |
| | | |
| | | } else { |
| | | |
| | | v.setFromMatrixColumn( objectMatrix, 0 ); |
| | | v.crossVectors( scope.object.up, v ); |
| | | |
| | | } |
| | | |
| | | v.multiplyScalar( distance ); |
| | | panOffset.add( v ); |
| | | |
| | | }; |
| | | |
| | | }(); // deltaX and deltaY are in pixels; right and down are positive |
| | | |
| | | |
| | | var pan = function () { |
| | | |
| | | var offset = new THREE.Vector3(); |
| | | return function pan( deltaX, deltaY ) { |
| | | |
| | | var element = scope.domElement; |
| | | |
| | | if ( scope.object.isPerspectiveCamera ) { |
| | | |
| | | // perspective |
| | | var position = scope.object.position; |
| | | offset.copy( position ).sub( scope.target ); |
| | | var targetDistance = offset.length(); // half of the fov is center to top of screen |
| | | |
| | | targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed |
| | | |
| | | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); |
| | | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); |
| | | |
| | | } else if ( scope.object.isOrthographicCamera ) { |
| | | |
| | | // orthographic |
| | | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); |
| | | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); |
| | | |
| | | } else { |
| | | |
| | | // camera neither orthographic nor perspective |
| | | console.warn( 'WARNING: CameraControls.js encountered an unknown camera type - pan disabled.' ); |
| | | scope.enablePan = false; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | function dollyIn( dollyScale ) { |
| | | |
| | | if ( scope.object.isPerspectiveCamera ) { |
| | | |
| | | scale /= dollyScale; |
| | | |
| | | } else if ( scope.object.isOrthographicCamera ) { |
| | | |
| | | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); |
| | | scope.object.updateProjectionMatrix(); |
| | | zoomChanged = true; |
| | | |
| | | } else { |
| | | |
| | | console.warn( 'WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.' ); |
| | | scope.enableZoom = false; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function dollyOut( dollyScale ) { |
| | | |
| | | if ( scope.object.isPerspectiveCamera ) { |
| | | |
| | | scale *= dollyScale; |
| | | |
| | | } else if ( scope.object.isOrthographicCamera ) { |
| | | |
| | | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); |
| | | scope.object.updateProjectionMatrix(); |
| | | zoomChanged = true; |
| | | |
| | | } else { |
| | | |
| | | console.warn( 'WARNING: CameraControls.js encountered an unknown camera type - dolly/zoom disabled.' ); |
| | | scope.enableZoom = false; |
| | | |
| | | } |
| | | |
| | | } // |
| | | // event callbacks - update the object state |
| | | // |
| | | |
| | | |
| | | function handleMouseDownRotate( event ) { |
| | | |
| | | rotateStart.set( event.clientX, event.clientY ); |
| | | |
| | | } |
| | | |
| | | function handleMouseDownDolly( event ) { |
| | | |
| | | 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 ); |
| | | var element = scope.domElement; |
| | | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height |
| | | |
| | | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); |
| | | rotateStart.copy( rotateEnd ); |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | function handleMouseMoveDolly( event ) { |
| | | |
| | | dollyEnd.set( event.clientX, event.clientY ); |
| | | dollyDelta.subVectors( dollyEnd, dollyStart ); |
| | | |
| | | if ( dollyDelta.y > 0 ) { |
| | | |
| | | dollyIn( getZoomScale() ); |
| | | |
| | | } else if ( dollyDelta.y < 0 ) { |
| | | |
| | | dollyOut( getZoomScale() ); |
| | | |
| | | } |
| | | |
| | | 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 handleMouseUp() { // no-op |
| | | } |
| | | |
| | | function handleMouseWheel( event ) { |
| | | |
| | | if ( event.deltaY < 0 ) { |
| | | |
| | | dollyOut( getZoomScale() ); |
| | | |
| | | } else if ( event.deltaY > 0 ) { |
| | | |
| | | dollyIn( getZoomScale() ); |
| | | |
| | | } |
| | | |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | function handleKeyDown( event ) { |
| | | |
| | | var needsUpdate = false; |
| | | |
| | | switch ( event.keyCode ) { |
| | | |
| | | case scope.keys.UP: |
| | | pan( 0, scope.keyPanSpeed ); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.BOTTOM: |
| | | pan( 0, - scope.keyPanSpeed ); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.LEFT: |
| | | pan( scope.keyPanSpeed, 0 ); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.RIGHT: |
| | | pan( - scope.keyPanSpeed, 0 ); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | } |
| | | |
| | | if ( needsUpdate ) { |
| | | |
| | | // prevent the browser from scrolling on cursor keys |
| | | event.preventDefault(); |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function handleTouchStartRotate( event ) { |
| | | |
| | | if ( event.touches.length == 1 ) { |
| | | |
| | | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); |
| | | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); |
| | | rotateStart.set( x, y ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function handleTouchStartPan( event ) { |
| | | |
| | | if ( event.touches.length == 1 ) { |
| | | |
| | | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); |
| | | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); |
| | | panStart.set( x, y ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function handleTouchStartDolly( event ) { |
| | | |
| | | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; |
| | | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; |
| | | var 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 ( event.touches.length == 1 ) { |
| | | |
| | | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); |
| | | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); |
| | | rotateEnd.set( x, y ); |
| | | |
| | | } |
| | | |
| | | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); |
| | | var element = scope.domElement; |
| | | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height |
| | | |
| | | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); |
| | | rotateStart.copy( rotateEnd ); |
| | | |
| | | } |
| | | |
| | | function handleTouchMovePan( event ) { |
| | | |
| | | if ( event.touches.length == 1 ) { |
| | | |
| | | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); |
| | | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); |
| | | panEnd.set( x, y ); |
| | | |
| | | } |
| | | |
| | | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); |
| | | pan( panDelta.x, panDelta.y ); |
| | | panStart.copy( panEnd ); |
| | | |
| | | } |
| | | |
| | | function handleTouchMoveDolly( event ) { |
| | | |
| | | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; |
| | | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; |
| | | var distance = Math.sqrt( dx * dx + dy * dy ); |
| | | dollyEnd.set( 0, distance ); |
| | | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); |
| | | dollyIn( dollyDelta.y ); |
| | | dollyStart.copy( dollyEnd ); |
| | | |
| | | } |
| | | |
| | | 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 ); |
| | | |
| | | } |
| | | |
| | | function handleTouchEnd() { // no-op |
| | | } // |
| | | // event handlers - FSM: listen for events and reset state |
| | | // |
| | | |
| | | |
| | | function onMouseDown( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; // Prevent the browser from scrolling. |
| | | |
| | | event.preventDefault(); // Manually set the focus since calling preventDefault above |
| | | // prevents the browser from setting it automatically. |
| | | |
| | | scope.domElement.focus ? scope.domElement.focus() : window.focus(); |
| | | var 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 THREE.MOUSE.DOLLY: |
| | | if ( scope.enableZoom === false ) return; |
| | | handleMouseDownDolly( event ); |
| | | state = STATE.DOLLY; |
| | | break; |
| | | |
| | | case THREE.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 THREE.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 ) { |
| | | |
| | | document.addEventListener( 'mousemove', onMouseMove, false ); |
| | | document.addEventListener( 'mouseup', onMouseUp, false ); |
| | | scope.dispatchEvent( startEvent ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function onMouseMove( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | event.preventDefault(); |
| | | |
| | | 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 onMouseUp( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | handleMouseUp( event ); |
| | | document.removeEventListener( 'mousemove', onMouseMove, false ); |
| | | document.removeEventListener( 'mouseup', onMouseUp, false ); |
| | | scope.dispatchEvent( endEvent ); |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | function onMouseWheel( event ) { |
| | | |
| | | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return; |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | scope.dispatchEvent( startEvent ); |
| | | handleMouseWheel( event ); |
| | | scope.dispatchEvent( endEvent ); |
| | | |
| | | } |
| | | |
| | | function onKeyDown( event ) { |
| | | |
| | | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; |
| | | handleKeyDown( event ); |
| | | |
| | | } |
| | | |
| | | function onTouchStart( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | event.preventDefault(); |
| | | |
| | | switch ( event.touches.length ) { |
| | | |
| | | case 1: |
| | | switch ( scope.touches.ONE ) { |
| | | |
| | | case THREE.TOUCH.ROTATE: |
| | | if ( scope.enableRotate === false ) return; |
| | | handleTouchStartRotate( event ); |
| | | state = STATE.TOUCH_ROTATE; |
| | | break; |
| | | |
| | | case THREE.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 THREE.TOUCH.DOLLY_PAN: |
| | | if ( scope.enableZoom === false && scope.enablePan === false ) return; |
| | | handleTouchStartDollyPan( event ); |
| | | state = STATE.TOUCH_DOLLY_PAN; |
| | | break; |
| | | |
| | | case THREE.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 ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | 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 onTouchEnd( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | handleTouchEnd( event ); |
| | | scope.dispatchEvent( endEvent ); |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | function onContextMenu( event ) { |
| | | |
| | | if ( scope.enabled === false ) return; |
| | | event.preventDefault(); |
| | | |
| | | } // |
| | | |
| | | |
| | | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); |
| | | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); |
| | | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); |
| | | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); |
| | | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); |
| | | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); |
| | | scope.domElement.addEventListener( 'keydown', onKeyDown, false ); // make sure element can receive keys. |
| | | |
| | | if ( scope.domElement.tabIndex === - 1 ) { |
| | | |
| | | scope.domElement.tabIndex = 0; |
| | | |
| | | } // force an update at start |
| | | |
| | | |
| | | this.object.lookAt( scope.target ); |
| | | this.update(); |
| | | this.saveState(); |
| | | |
| | | }; |
| | | |
| | | CameraControls.prototype = Object.create( THREE.EventDispatcher.prototype ); |
| | | CameraControls.prototype.constructor = CameraControls; // OrbitControls maintains the "up" direction, camera.up (+Y by default). |
| | | // |
| | | // Orbit - left mouse / touch: one-finger move |
| | | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish |
| | | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move |
| | | |
| | | var OrbitControls = function ( object, domElement ) { |
| | | |
| | | CameraControls.call( this, object, domElement ); |
| | | this.mouseButtons.LEFT = THREE.MOUSE.ROTATE; |
| | | this.mouseButtons.RIGHT = THREE.MOUSE.PAN; |
| | | this.touches.ONE = THREE.TOUCH.ROTATE; |
| | | this.touches.TWO = THREE.TOUCH.DOLLY_PAN; |
| | | |
| | | }; |
| | | |
| | | OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); |
| | | OrbitControls.prototype.constructor = OrbitControls; // MapControls maintains the "up" direction, camera.up (+Y by default) |
| | | // |
| | | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate |
| | | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish |
| | | // Pan - left mouse, or left right + ctrl/meta/shiftKey, or arrow keys / touch: one-finger move |
| | | |
| | | var MapControls = function ( object, domElement ) { |
| | | |
| | | CameraControls.call( this, object, domElement ); |
| | | this.mouseButtons.LEFT = THREE.MOUSE.PAN; |
| | | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; |
| | | this.touches.ONE = THREE.TOUCH.PAN; |
| | | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; |
| | | |
| | | }; |
| | | |
| | | MapControls.prototype = Object.create( THREE.EventDispatcher.prototype ); |
| | | MapControls.prototype.constructor = MapControls; // TrackballControls allows the camera to rotate over the polls and does not maintain camera.up |
| | | // |
| | | // Orbit - left mouse / touch: one-finger move |
| | | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish |
| | | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move |
| | | |
| | | var TrackballControls = function ( object, domElement ) { |
| | | |
| | | CameraControls.call( this, object, domElement ); |
| | | this.trackball = true; |
| | | this.screenSpacePanning = true; |
| | | this.autoRotate = false; |
| | | this.mouseButtons.LEFT = THREE.MOUSE.ROTATE; |
| | | this.mouseButtons.RIGHT = THREE.MOUSE.PAN; |
| | | this.touches.ONE = THREE.TOUCH.ROTATE; |
| | | this.touches.TWO = THREE.TOUCH.DOLLY_PAN; |
| | | |
| | | }; |
| | | |
| | | TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); |
| | | TrackballControls.prototype.constructor = TrackballControls; |
| | | |
| | | THREE.CameraControls = CameraControls; |
| | | THREE.MapControls = MapControls; |
| | | THREE.OrbitControls = OrbitControls; |
| | | THREE.TrackballControls = TrackballControls; |
| | | |
| | | } )(); |
| New file |
| | |
| | | /** |
| | | * @author qiao / https://github.com/qiao |
| | | * @author mrdoob / http://mrdoob.com |
| | | * @author alteredq / http://alteredqualia.com/ |
| | | * @author WestLangley / http://github.com/WestLangley |
| | | * @author erich666 / http://erichaines.com |
| | | * @author ScieCode / http://github.com/sciecode |
| | | */ |
| | | |
| | | import { |
| | | EventDispatcher, |
| | | MOUSE, |
| | | Quaternion, |
| | | Spherical, |
| | | TOUCH, |
| | | Vector2, |
| | | Vector3, |
| | | global as window |
| | | } from "../../libs/three.weapp.js"; |
| | | |
| | | // This set of controls performs orbiting, dollying (zooming), and panning. |
| | | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). |
| | | // |
| | | // Orbit - left mouse / touch: one-finger move |
| | | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish |
| | | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move |
| | | |
| | | var OrbitControls = function (object, domElement) { |
| | | |
| | | this.object = object; |
| | | |
| | | this.domElement = (domElement !== undefined) ? domElement : document; |
| | | |
| | | // Set to false to disable this control |
| | | this.enabled = true; |
| | | |
| | | // "target" sets the location of focus, where the object orbits around |
| | | this.target = new Vector3(); |
| | | |
| | | // 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; |
| | | |
| | | // How far you can orbit vertically, upper and lower limits. |
| | | // Range is 0 to Math.PI radians. |
| | | this.minPolarAngle = 0; // radians |
| | | this.maxPolarAngle = Math.PI; // radians |
| | | |
| | | // How far you can orbit horizontally, upper and lower limits. |
| | | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. |
| | | this.minAzimuthAngle = - Infinity; // radians |
| | | this.maxAzimuthAngle = Infinity; // radians |
| | | |
| | | // Set to true to enable damping (inertia) |
| | | // If damping is enabled, you must call controls.update() in your animation loop |
| | | this.enableDamping = false; |
| | | this.dampingFactor = 0.05; |
| | | |
| | | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. |
| | | // Set to false to disable zooming |
| | | this.enableZoom = true; |
| | | this.zoomSpeed = 1.0; |
| | | |
| | | // Set to false to disable rotating |
| | | this.enableRotate = true; |
| | | this.rotateSpeed = 1.0; |
| | | |
| | | // Set to false to disable panning |
| | | this.enablePan = true; |
| | | this.panSpeed = 1.0; |
| | | this.screenSpacePanning = false; // if true, pan in screen-space |
| | | this.keyPanSpeed = 7.0; // pixels moved per arrow key push |
| | | |
| | | // Set to true to automatically rotate around the target |
| | | // If auto-rotate is enabled, you must call controls.update() in your animation loop |
| | | this.autoRotate = false; |
| | | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 |
| | | |
| | | // Set to false to disable use of the keys |
| | | this.enableKeys = true; |
| | | |
| | | // The four arrow keys |
| | | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; |
| | | |
| | | // Mouse buttons |
| | | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; |
| | | |
| | | // Touch fingers |
| | | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; |
| | | |
| | | // for reset |
| | | this.target0 = this.target.clone(); |
| | | this.position0 = this.object.position.clone(); |
| | | this.zoom0 = this.object.zoom; |
| | | |
| | | // |
| | | // public methods |
| | | // |
| | | |
| | | this.getPolarAngle = function () { |
| | | |
| | | return spherical.phi; |
| | | |
| | | }; |
| | | |
| | | this.getAzimuthalAngle = function () { |
| | | |
| | | return spherical.theta; |
| | | |
| | | }; |
| | | |
| | | 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 () { |
| | | |
| | | var offset = new Vector3(); |
| | | |
| | | // so camera.up is the orbit axis |
| | | var quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); |
| | | var quatInverse = quat.clone().inverse(); |
| | | |
| | | var lastPosition = new Vector3(); |
| | | var lastQuaternion = new Quaternion(); |
| | | |
| | | return function update() { |
| | | |
| | | var position = scope.object.position; |
| | | |
| | | offset.copy(position).sub(scope.target); |
| | | |
| | | // rotate offset to "y-axis-is-up" space |
| | | offset.applyQuaternion(quat); |
| | | |
| | | // angle from z-axis around y-axis |
| | | spherical.setFromVector3(offset); |
| | | |
| | | if (scope.autoRotate && state === STATE.NONE) { |
| | | |
| | | rotateLeft(getAutoRotationAngle()); |
| | | |
| | | } |
| | | |
| | | if (scope.enableDamping) { |
| | | |
| | | spherical.theta += sphericalDelta.theta * scope.dampingFactor; |
| | | spherical.phi += sphericalDelta.phi * scope.dampingFactor; |
| | | |
| | | } else { |
| | | |
| | | spherical.theta += sphericalDelta.theta; |
| | | spherical.phi += sphericalDelta.phi; |
| | | |
| | | } |
| | | |
| | | // restrict theta to be between desired limits |
| | | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); |
| | | |
| | | // restrict phi to be between desired limits |
| | | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); |
| | | |
| | | spherical.makeSafe(); |
| | | |
| | | |
| | | spherical.radius *= scale; |
| | | |
| | | // restrict radius to be between desired limits |
| | | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); |
| | | |
| | | // move target to panned location |
| | | |
| | | if (scope.enableDamping === true) { |
| | | |
| | | scope.target.addScaledVector(panOffset, scope.dampingFactor); |
| | | |
| | | } else { |
| | | |
| | | scope.target.add(panOffset); |
| | | |
| | | } |
| | | |
| | | offset.setFromSpherical(spherical); |
| | | |
| | | // rotate offset back to "camera-up-vector-is-up" space |
| | | offset.applyQuaternion(quatInverse); |
| | | |
| | | position.copy(scope.target).add(offset); |
| | | |
| | | scope.object.lookAt(scope.target); |
| | | |
| | | if (scope.enableDamping === true) { |
| | | |
| | | sphericalDelta.theta *= (1 - scope.dampingFactor); |
| | | sphericalDelta.phi *= (1 - scope.dampingFactor); |
| | | |
| | | panOffset.multiplyScalar(1 - scope.dampingFactor); |
| | | |
| | | } else { |
| | | |
| | | sphericalDelta.set(0, 0, 0); |
| | | |
| | | panOffset.set(0, 0, 0); |
| | | |
| | | } |
| | | |
| | | scale = 1; |
| | | |
| | | // update condition is: |
| | | // min(camera displacement, camera rotation in radians)^2 > EPS |
| | | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 |
| | | |
| | | if (zoomChanged || |
| | | lastPosition.distanceToSquared(scope.object.position) > EPS || |
| | | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { |
| | | |
| | | scope.dispatchEvent(changeEvent); |
| | | |
| | | lastPosition.copy(scope.object.position); |
| | | lastQuaternion.copy(scope.object.quaternion); |
| | | zoomChanged = false; |
| | | |
| | | return true; |
| | | |
| | | } |
| | | |
| | | return false; |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | this.dispose = function () { |
| | | |
| | | scope.domElement.removeEventListener('contextmenu', onContextMenu, false); |
| | | scope.domElement.removeEventListener('mousedown', onMouseDown, false); |
| | | scope.domElement.removeEventListener('wheel', onMouseWheel, false); |
| | | |
| | | scope.domElement.removeEventListener('touchstart', onTouchStart, false); |
| | | scope.domElement.removeEventListener('touchend', onTouchEnd, false); |
| | | scope.domElement.removeEventListener('touchmove', onTouchMove, false); |
| | | |
| | | document.removeEventListener('mousemove', onMouseMove, false); |
| | | document.removeEventListener('mouseup', onMouseUp, false); |
| | | |
| | | window.removeEventListener('keydown', onKeyDown, false); |
| | | |
| | | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? |
| | | |
| | | }; |
| | | |
| | | // |
| | | // internals |
| | | // |
| | | |
| | | var scope = this; |
| | | |
| | | var changeEvent = { type: 'change' }; |
| | | var startEvent = { type: 'start' }; |
| | | var endEvent = { type: 'end' }; |
| | | |
| | | var STATE = { |
| | | NONE: - 1, |
| | | ROTATE: 0, |
| | | DOLLY: 1, |
| | | PAN: 2, |
| | | TOUCH_ROTATE: 3, |
| | | TOUCH_PAN: 4, |
| | | TOUCH_DOLLY_PAN: 5, |
| | | TOUCH_DOLLY_ROTATE: 6 |
| | | }; |
| | | |
| | | var state = STATE.NONE; |
| | | |
| | | var EPS = 0.000001; |
| | | |
| | | // current position in spherical coordinates |
| | | var spherical = new Spherical(); |
| | | var sphericalDelta = new Spherical(); |
| | | |
| | | var scale = 1; |
| | | var panOffset = new Vector3(); |
| | | var zoomChanged = false; |
| | | |
| | | var rotateStart = new Vector2(); |
| | | var rotateEnd = new Vector2(); |
| | | var rotateDelta = new Vector2(); |
| | | |
| | | var panStart = new Vector2(); |
| | | var panEnd = new Vector2(); |
| | | var panDelta = new Vector2(); |
| | | |
| | | var dollyStart = new Vector2(); |
| | | var dollyEnd = new Vector2(); |
| | | var dollyDelta = new Vector2(); |
| | | |
| | | function getAutoRotationAngle() { |
| | | |
| | | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; |
| | | |
| | | } |
| | | |
| | | function getZoomScale() { |
| | | |
| | | return Math.pow(0.95, scope.zoomSpeed); |
| | | |
| | | } |
| | | |
| | | function rotateLeft(angle) { |
| | | |
| | | sphericalDelta.theta -= angle; |
| | | |
| | | } |
| | | |
| | | function rotateUp(angle) { |
| | | |
| | | sphericalDelta.phi -= angle; |
| | | |
| | | } |
| | | |
| | | var panLeft = function () { |
| | | |
| | | var v = new Vector3(); |
| | | |
| | | return function panLeft(distance, objectMatrix) { |
| | | |
| | | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix |
| | | v.multiplyScalar(- distance); |
| | | |
| | | panOffset.add(v); |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | var panUp = function () { |
| | | |
| | | var v = new Vector3(); |
| | | |
| | | return function panUp(distance, objectMatrix) { |
| | | |
| | | if (scope.screenSpacePanning === true) { |
| | | |
| | | v.setFromMatrixColumn(objectMatrix, 1); |
| | | |
| | | } else { |
| | | |
| | | v.setFromMatrixColumn(objectMatrix, 0); |
| | | v.crossVectors(scope.object.up, v); |
| | | |
| | | } |
| | | |
| | | v.multiplyScalar(distance); |
| | | |
| | | panOffset.add(v); |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | // deltaX and deltaY are in pixels; right and down are positive |
| | | var pan = function () { |
| | | |
| | | var offset = new Vector3(); |
| | | |
| | | return function pan(deltaX, deltaY) { |
| | | |
| | | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; |
| | | |
| | | if (scope.object.isPerspectiveCamera) { |
| | | |
| | | // perspective |
| | | var position = scope.object.position; |
| | | offset.copy(position).sub(scope.target); |
| | | var targetDistance = offset.length(); |
| | | |
| | | // half of the fov is center to top of screen |
| | | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); |
| | | |
| | | // we use only clientHeight here so aspect ratio does not distort speed |
| | | panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); |
| | | panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); |
| | | |
| | | } else if (scope.object.isOrthographicCamera) { |
| | | |
| | | // orthographic |
| | | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); |
| | | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); |
| | | |
| | | } else { |
| | | |
| | | // camera neither orthographic nor perspective |
| | | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); |
| | | scope.enablePan = false; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | function dollyIn(dollyScale) { |
| | | |
| | | if (scope.object.isPerspectiveCamera) { |
| | | |
| | | scale /= dollyScale; |
| | | |
| | | } else if (scope.object.isOrthographicCamera) { |
| | | |
| | | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); |
| | | scope.object.updateProjectionMatrix(); |
| | | zoomChanged = true; |
| | | |
| | | } else { |
| | | |
| | | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); |
| | | scope.enableZoom = false; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function dollyOut(dollyScale) { |
| | | |
| | | if (scope.object.isPerspectiveCamera) { |
| | | |
| | | scale *= dollyScale; |
| | | |
| | | } else if (scope.object.isOrthographicCamera) { |
| | | |
| | | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); |
| | | scope.object.updateProjectionMatrix(); |
| | | zoomChanged = true; |
| | | |
| | | } else { |
| | | |
| | | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); |
| | | scope.enableZoom = false; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // |
| | | // event callbacks - update the object state |
| | | // |
| | | |
| | | function handleMouseDownRotate(event) { |
| | | |
| | | rotateStart.set(event.clientX, event.clientY); |
| | | |
| | | } |
| | | |
| | | function handleMouseDownDolly(event) { |
| | | |
| | | 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); |
| | | |
| | | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; |
| | | |
| | | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height |
| | | |
| | | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); |
| | | |
| | | rotateStart.copy(rotateEnd); |
| | | |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | function handleMouseMoveDolly(event) { |
| | | |
| | | dollyEnd.set(event.clientX, event.clientY); |
| | | |
| | | dollyDelta.subVectors(dollyEnd, dollyStart); |
| | | |
| | | if (dollyDelta.y > 0) { |
| | | |
| | | dollyIn(getZoomScale()); |
| | | |
| | | } else if (dollyDelta.y < 0) { |
| | | |
| | | dollyOut(getZoomScale()); |
| | | |
| | | } |
| | | |
| | | 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 handleMouseUp( /*event*/) { |
| | | |
| | | // no-op |
| | | |
| | | } |
| | | |
| | | function handleMouseWheel(event) { |
| | | |
| | | if (event.deltaY < 0) { |
| | | |
| | | dollyOut(getZoomScale()); |
| | | |
| | | } else if (event.deltaY > 0) { |
| | | |
| | | dollyIn(getZoomScale()); |
| | | |
| | | } |
| | | |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | function handleKeyDown(event) { |
| | | |
| | | var needsUpdate = false; |
| | | |
| | | switch (event.keyCode) { |
| | | |
| | | case scope.keys.UP: |
| | | pan(0, scope.keyPanSpeed); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.BOTTOM: |
| | | pan(0, - scope.keyPanSpeed); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.LEFT: |
| | | pan(scope.keyPanSpeed, 0); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | case scope.keys.RIGHT: |
| | | pan(- scope.keyPanSpeed, 0); |
| | | needsUpdate = true; |
| | | break; |
| | | |
| | | } |
| | | |
| | | if (needsUpdate) { |
| | | |
| | | // prevent the browser from scrolling on cursor keys |
| | | event.preventDefault(); |
| | | |
| | | scope.update(); |
| | | |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | function handleTouchStartRotate(event) { |
| | | |
| | | if (event.touches.length == 1) { |
| | | |
| | | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); |
| | | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); |
| | | |
| | | rotateStart.set(x, y); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function handleTouchStartPan(event) { |
| | | |
| | | if (event.touches.length == 1) { |
| | | |
| | | panStart.set(event.touches[0].pageX, event.touches[0].pageY); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); |
| | | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); |
| | | |
| | | panStart.set(x, y); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function handleTouchStartDolly(event) { |
| | | |
| | | var dx = event.touches[0].pageX - event.touches[1].pageX; |
| | | var dy = event.touches[0].pageY - event.touches[1].pageY; |
| | | |
| | | var 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 (event.touches.length == 1) { |
| | | |
| | | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); |
| | | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); |
| | | |
| | | rotateEnd.set(x, y); |
| | | |
| | | } |
| | | |
| | | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); |
| | | |
| | | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; |
| | | |
| | | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height |
| | | |
| | | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); |
| | | |
| | | rotateStart.copy(rotateEnd); |
| | | |
| | | } |
| | | |
| | | function handleTouchMovePan(event) { |
| | | |
| | | if (event.touches.length == 1) { |
| | | |
| | | panEnd.set(event.touches[0].pageX, event.touches[0].pageY); |
| | | |
| | | } else { |
| | | |
| | | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); |
| | | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); |
| | | |
| | | panEnd.set(x, y); |
| | | |
| | | } |
| | | |
| | | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); |
| | | |
| | | pan(panDelta.x, panDelta.y); |
| | | |
| | | panStart.copy(panEnd); |
| | | |
| | | } |
| | | |
| | | function handleTouchMoveDolly(event) { |
| | | |
| | | var dx = event.touches[0].pageX - event.touches[1].pageX; |
| | | var dy = event.touches[0].pageY - event.touches[1].pageY; |
| | | |
| | | var distance = Math.sqrt(dx * dx + dy * dy); |
| | | |
| | | dollyEnd.set(0, distance); |
| | | |
| | | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); |
| | | |
| | | dollyIn(dollyDelta.y); |
| | | |
| | | dollyStart.copy(dollyEnd); |
| | | |
| | | } |
| | | |
| | | 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); |
| | | |
| | | } |
| | | |
| | | function handleTouchEnd( /*event*/) { |
| | | |
| | | // no-op |
| | | |
| | | } |
| | | |
| | | // |
| | | // event handlers - FSM: listen for events and reset state |
| | | // |
| | | |
| | | function onMouseDown(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | // Prevent the browser from scrolling. |
| | | |
| | | event.preventDefault(); |
| | | |
| | | // Manually set the focus since calling preventDefault above |
| | | // prevents the browser from setting it automatically. |
| | | |
| | | scope.domElement.focus ? scope.domElement.focus() : window.focus(); |
| | | |
| | | switch (event.button) { |
| | | |
| | | case 0: |
| | | |
| | | switch (scope.mouseButtons.LEFT) { |
| | | |
| | | 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; |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | |
| | | case 1: |
| | | |
| | | switch (scope.mouseButtons.MIDDLE) { |
| | | |
| | | case MOUSE.DOLLY: |
| | | |
| | | if (scope.enableZoom === false) return; |
| | | |
| | | handleMouseDownDolly(event); |
| | | |
| | | state = STATE.DOLLY; |
| | | |
| | | break; |
| | | |
| | | |
| | | default: |
| | | |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | case 2: |
| | | |
| | | switch (scope.mouseButtons.RIGHT) { |
| | | |
| | | case MOUSE.ROTATE: |
| | | |
| | | if (scope.enableRotate === false) return; |
| | | |
| | | handleMouseDownRotate(event); |
| | | |
| | | state = STATE.ROTATE; |
| | | |
| | | break; |
| | | |
| | | case MOUSE.PAN: |
| | | |
| | | if (scope.enablePan === false) return; |
| | | |
| | | handleMouseDownPan(event); |
| | | |
| | | state = STATE.PAN; |
| | | |
| | | break; |
| | | |
| | | default: |
| | | |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | } |
| | | |
| | | if (state !== STATE.NONE) { |
| | | |
| | | document.addEventListener('mousemove', onMouseMove, false); |
| | | document.addEventListener('mouseup', onMouseUp, false); |
| | | |
| | | scope.dispatchEvent(startEvent); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function onMouseMove(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | |
| | | 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 onMouseUp(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | handleMouseUp(event); |
| | | |
| | | document.removeEventListener('mousemove', onMouseMove, false); |
| | | document.removeEventListener('mouseup', onMouseUp, false); |
| | | |
| | | scope.dispatchEvent(endEvent); |
| | | |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | function onMouseWheel(event) { |
| | | |
| | | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | scope.dispatchEvent(startEvent); |
| | | |
| | | handleMouseWheel(event); |
| | | |
| | | scope.dispatchEvent(endEvent); |
| | | |
| | | } |
| | | |
| | | function onKeyDown(event) { |
| | | |
| | | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return; |
| | | |
| | | handleKeyDown(event); |
| | | |
| | | } |
| | | |
| | | function onTouchStart(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | |
| | | switch (event.touches.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) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | 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 onTouchEnd(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | handleTouchEnd(event); |
| | | |
| | | scope.dispatchEvent(endEvent); |
| | | |
| | | state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | function onContextMenu(event) { |
| | | |
| | | if (scope.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | |
| | | } |
| | | |
| | | // |
| | | |
| | | scope.domElement.addEventListener('contextmenu', onContextMenu, false); |
| | | |
| | | scope.domElement.addEventListener('mousedown', onMouseDown, false); |
| | | scope.domElement.addEventListener('wheel', onMouseWheel, false); |
| | | |
| | | scope.domElement.addEventListener('touchstart', onTouchStart, false); |
| | | scope.domElement.addEventListener('touchend', onTouchEnd, false); |
| | | scope.domElement.addEventListener('touchmove', onTouchMove, false); |
| | | |
| | | window.addEventListener('keydown', onKeyDown, false); |
| | | |
| | | // force an update at start |
| | | |
| | | this.update(); |
| | | |
| | | }; |
| | | |
| | | OrbitControls.prototype = Object.create(EventDispatcher.prototype); |
| | | OrbitControls.prototype.constructor = OrbitControls; |
| | | |
| | | |
| | | // This set of controls performs orbiting, dollying (zooming), and panning. |
| | | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). |
| | | // This is very similar to OrbitControls, another set of touch behavior |
| | | // |
| | | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate |
| | | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish |
| | | // Pan - left mouse, or arrow keys / touch: one-finger move |
| | | |
| | | var MapControls = function (object, domElement) { |
| | | |
| | | OrbitControls.call(this, object, domElement); |
| | | |
| | | this.mouseButtons.LEFT = MOUSE.PAN; |
| | | this.mouseButtons.RIGHT = MOUSE.ROTATE; |
| | | |
| | | this.touches.ONE = TOUCH.PAN; |
| | | this.touches.TWO = TOUCH.DOLLY_ROTATE; |
| | | |
| | | }; |
| | | |
| | | MapControls.prototype = Object.create(EventDispatcher.prototype); |
| | | MapControls.prototype.constructor = MapControls; |
| | | |
| | | export { OrbitControls, MapControls }; |
| New file |
| | |
| | | /** |
| | | * @author Eberhard Graether / http://egraether.com/ |
| | | * @author Mark Lundin / http://mark-lundin.com |
| | | * @author Simone Manini / http://daron1337.github.io |
| | | * @author Luca Antiga / http://lantiga.github.io |
| | | */ |
| | | export default function (THREE) { |
| | | |
| | | let { |
| | | EventDispatcher, |
| | | MOUSE, |
| | | Quaternion, |
| | | Vector2, |
| | | Vector3, |
| | | global: window, |
| | | } = THREE; |
| | | let document = window.document; |
| | | |
| | | |
| | | var TrackballControls = function (object, domElement) { |
| | | |
| | | if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.'); |
| | | if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); |
| | | |
| | | var _this = this; |
| | | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; |
| | | |
| | | this.object = object; |
| | | this.domElement = domElement; |
| | | |
| | | // API |
| | | |
| | | this.enabled = true; |
| | | |
| | | this.screen = { left: 0, top: 0, width: 0, height: 0 }; |
| | | |
| | | this.rotateSpeed = 1.0; |
| | | this.zoomSpeed = 1.2; |
| | | this.panSpeed = 0.3; |
| | | |
| | | this.noRotate = false; |
| | | this.noZoom = false; |
| | | this.noPan = false; |
| | | |
| | | this.staticMoving = false; |
| | | this.dynamicDampingFactor = 0.2; |
| | | |
| | | this.minDistance = 0; |
| | | this.maxDistance = Infinity; |
| | | |
| | | this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/]; |
| | | |
| | | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.ZOOM, RIGHT: MOUSE.PAN }; |
| | | |
| | | // internals |
| | | |
| | | this.target = new Vector3(); |
| | | |
| | | var EPS = 0.000001; |
| | | |
| | | var lastPosition = new Vector3(); |
| | | var lastZoom = 1; |
| | | |
| | | var _state = STATE.NONE, |
| | | _keyState = STATE.NONE, |
| | | |
| | | _eye = new Vector3(), |
| | | |
| | | _movePrev = new Vector2(), |
| | | _moveCurr = new Vector2(), |
| | | |
| | | _lastAxis = new Vector3(), |
| | | _lastAngle = 0, |
| | | |
| | | _zoomStart = new Vector2(), |
| | | _zoomEnd = new Vector2(), |
| | | |
| | | _touchZoomDistanceStart = 0, |
| | | _touchZoomDistanceEnd = 0, |
| | | |
| | | _panStart = new Vector2(), |
| | | _panEnd = new Vector2(); |
| | | |
| | | // for reset |
| | | |
| | | this.target0 = this.target.clone(); |
| | | this.position0 = this.object.position.clone(); |
| | | this.up0 = this.object.up.clone(); |
| | | this.zoom0 = this.object.zoom; |
| | | |
| | | // events |
| | | |
| | | var changeEvent = { type: 'change' }; |
| | | var startEvent = { type: 'start' }; |
| | | var endEvent = { type: 'end' }; |
| | | |
| | | |
| | | // methods |
| | | |
| | | this.handleResize = function () { |
| | | |
| | | var box = this.domElement.getBoundingClientRect(); |
| | | // adjustments come from similar code in the jquery offset() function |
| | | // var d = this.domElement.ownerDocument.documentElement; |
| | | var d = document.documentElement; |
| | | this.screen.left = box.left + window.scrollX - d.clientLeft; |
| | | this.screen.top = box.top + window.scrollY - d.clientTop; |
| | | this.screen.width = box.width; |
| | | this.screen.height = box.height; |
| | | |
| | | }; |
| | | |
| | | var getMouseOnScreen = (function () { |
| | | |
| | | var vector = new Vector2(); |
| | | |
| | | return function getMouseOnScreen(pageX, pageY) { |
| | | |
| | | vector.set( |
| | | (pageX - _this.screen.left) / _this.screen.width, |
| | | (pageY - _this.screen.top) / _this.screen.height |
| | | ); |
| | | |
| | | return vector; |
| | | |
| | | }; |
| | | |
| | | }()); |
| | | |
| | | var getMouseOnCircle = (function () { |
| | | |
| | | var vector = new Vector2(); |
| | | |
| | | return function getMouseOnCircle(pageX, pageY) { |
| | | |
| | | vector.set( |
| | | ((pageX - _this.screen.width * 0.5 - _this.screen.left) / (_this.screen.width * 0.5)), |
| | | ((_this.screen.height + 2 * (_this.screen.top - pageY)) / _this.screen.width) // screen.width intentional |
| | | ); |
| | | |
| | | return vector; |
| | | |
| | | }; |
| | | |
| | | }()); |
| | | |
| | | this.rotateCamera = (function () { |
| | | |
| | | var axis = new Vector3(), |
| | | quaternion = new Quaternion(), |
| | | eyeDirection = new Vector3(), |
| | | objectUpDirection = new Vector3(), |
| | | objectSidewaysDirection = new Vector3(), |
| | | moveDirection = new Vector3(), |
| | | angle; |
| | | |
| | | return function rotateCamera() { |
| | | |
| | | moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0); |
| | | angle = moveDirection.length(); |
| | | |
| | | if (angle) { |
| | | |
| | | _eye.copy(_this.object.position).sub(_this.target); |
| | | |
| | | eyeDirection.copy(_eye).normalize(); |
| | | objectUpDirection.copy(_this.object.up).normalize(); |
| | | objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize(); |
| | | |
| | | objectUpDirection.setLength(_moveCurr.y - _movePrev.y); |
| | | objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x); |
| | | |
| | | moveDirection.copy(objectUpDirection.add(objectSidewaysDirection)); |
| | | |
| | | axis.crossVectors(moveDirection, _eye).normalize(); |
| | | |
| | | angle *= _this.rotateSpeed; |
| | | quaternion.setFromAxisAngle(axis, angle); |
| | | |
| | | _eye.applyQuaternion(quaternion); |
| | | _this.object.up.applyQuaternion(quaternion); |
| | | |
| | | _lastAxis.copy(axis); |
| | | _lastAngle = angle; |
| | | |
| | | } else if (!_this.staticMoving && _lastAngle) { |
| | | |
| | | _lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor); |
| | | _eye.copy(_this.object.position).sub(_this.target); |
| | | quaternion.setFromAxisAngle(_lastAxis, _lastAngle); |
| | | _eye.applyQuaternion(quaternion); |
| | | _this.object.up.applyQuaternion(quaternion); |
| | | |
| | | } |
| | | |
| | | _movePrev.copy(_moveCurr); |
| | | |
| | | }; |
| | | |
| | | }()); |
| | | |
| | | |
| | | this.zoomCamera = function () { |
| | | |
| | | var factor; |
| | | |
| | | if (_state === STATE.TOUCH_ZOOM_PAN) { |
| | | |
| | | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; |
| | | _touchZoomDistanceStart = _touchZoomDistanceEnd; |
| | | |
| | | if (_this.object.isPerspectiveCamera) { |
| | | |
| | | _eye.multiplyScalar(factor); |
| | | |
| | | } else if (_this.object.isOrthographicCamera) { |
| | | |
| | | _this.object.zoom *= factor; |
| | | _this.object.updateProjectionMatrix(); |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.TrackballControls: Unsupported camera type'); |
| | | |
| | | } |
| | | |
| | | } else { |
| | | |
| | | factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed; |
| | | |
| | | if (factor !== 1.0 && factor > 0.0) { |
| | | |
| | | if (_this.object.isPerspectiveCamera) { |
| | | |
| | | _eye.multiplyScalar(factor); |
| | | |
| | | } else if (_this.object.isOrthographicCamera) { |
| | | |
| | | _this.object.zoom /= factor; |
| | | _this.object.updateProjectionMatrix(); |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.TrackballControls: Unsupported camera type'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (_this.staticMoving) { |
| | | |
| | | _zoomStart.copy(_zoomEnd); |
| | | |
| | | } else { |
| | | |
| | | _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | this.panCamera = (function () { |
| | | |
| | | var mouseChange = new Vector2(), |
| | | objectUp = new Vector3(), |
| | | pan = new Vector3(); |
| | | |
| | | return function panCamera() { |
| | | |
| | | mouseChange.copy(_panEnd).sub(_panStart); |
| | | |
| | | if (mouseChange.lengthSq()) { |
| | | |
| | | if (_this.object.isOrthographicCamera) { |
| | | |
| | | var scale_x = (_this.object.right - _this.object.left) / _this.object.zoom / _this.domElement.clientWidth; |
| | | var scale_y = (_this.object.top - _this.object.bottom) / _this.object.zoom / _this.domElement.clientWidth; |
| | | |
| | | mouseChange.x *= scale_x; |
| | | mouseChange.y *= scale_y; |
| | | |
| | | } |
| | | |
| | | mouseChange.multiplyScalar(_eye.length() * _this.panSpeed); |
| | | |
| | | pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x); |
| | | pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y)); |
| | | |
| | | _this.object.position.add(pan); |
| | | _this.target.add(pan); |
| | | |
| | | if (_this.staticMoving) { |
| | | |
| | | _panStart.copy(_panEnd); |
| | | |
| | | } else { |
| | | |
| | | _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(_this.dynamicDampingFactor)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | }()); |
| | | |
| | | this.checkDistances = function () { |
| | | |
| | | if (!_this.noZoom || !_this.noPan) { |
| | | |
| | | if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) { |
| | | |
| | | _this.object.position.addVectors(_this.target, _eye.setLength(_this.maxDistance)); |
| | | _zoomStart.copy(_zoomEnd); |
| | | |
| | | } |
| | | |
| | | if (_eye.lengthSq() < _this.minDistance * _this.minDistance) { |
| | | |
| | | _this.object.position.addVectors(_this.target, _eye.setLength(_this.minDistance)); |
| | | _zoomStart.copy(_zoomEnd); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | this.update = function () { |
| | | |
| | | _eye.subVectors(_this.object.position, _this.target); |
| | | |
| | | if (!_this.noRotate) { |
| | | |
| | | _this.rotateCamera(); |
| | | |
| | | } |
| | | |
| | | if (!_this.noZoom) { |
| | | |
| | | _this.zoomCamera(); |
| | | |
| | | } |
| | | |
| | | if (!_this.noPan) { |
| | | |
| | | _this.panCamera(); |
| | | |
| | | } |
| | | |
| | | _this.object.position.addVectors(_this.target, _eye); |
| | | |
| | | if (_this.object.isPerspectiveCamera) { |
| | | |
| | | _this.checkDistances(); |
| | | |
| | | _this.object.lookAt(_this.target); |
| | | |
| | | if (lastPosition.distanceToSquared(_this.object.position) > EPS) { |
| | | |
| | | _this.dispatchEvent(changeEvent); |
| | | |
| | | lastPosition.copy(_this.object.position); |
| | | |
| | | } |
| | | |
| | | } else if (_this.object.isOrthographicCamera) { |
| | | |
| | | _this.object.lookAt(_this.target); |
| | | |
| | | if (lastPosition.distanceToSquared(_this.object.position) > EPS || lastZoom !== _this.object.zoom) { |
| | | |
| | | _this.dispatchEvent(changeEvent); |
| | | |
| | | lastPosition.copy(_this.object.position); |
| | | lastZoom = _this.object.zoom; |
| | | |
| | | } |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.TrackballControls: Unsupported camera type'); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | this.reset = function () { |
| | | |
| | | _state = STATE.NONE; |
| | | _keyState = STATE.NONE; |
| | | |
| | | _this.target.copy(_this.target0); |
| | | _this.object.position.copy(_this.position0); |
| | | _this.object.up.copy(_this.up0); |
| | | _this.object.zoom = _this.zoom0; |
| | | |
| | | _this.object.updateProjectionMatrix(); |
| | | |
| | | _eye.subVectors(_this.object.position, _this.target); |
| | | |
| | | _this.object.lookAt(_this.target); |
| | | |
| | | _this.dispatchEvent(changeEvent); |
| | | |
| | | lastPosition.copy(_this.object.position); |
| | | lastZoom = _this.object.zoom; |
| | | |
| | | }; |
| | | |
| | | // listeners |
| | | |
| | | function keydown(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | window.removeEventListener('keydown', keydown); |
| | | |
| | | if (_keyState !== STATE.NONE) { |
| | | |
| | | return; |
| | | |
| | | } else if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) { |
| | | |
| | | _keyState = STATE.ROTATE; |
| | | |
| | | } else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) { |
| | | |
| | | _keyState = STATE.ZOOM; |
| | | |
| | | } else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) { |
| | | |
| | | _keyState = STATE.PAN; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function keyup() { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | _keyState = STATE.NONE; |
| | | |
| | | window.addEventListener('keydown', keydown, false); |
| | | |
| | | } |
| | | |
| | | function mousedown(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | if (_state === STATE.NONE) { |
| | | |
| | | switch (event.button) { |
| | | |
| | | case _this.mouseButtons.LEFT: |
| | | _state = STATE.ROTATE; |
| | | break; |
| | | |
| | | case _this.mouseButtons.MIDDLE: |
| | | _state = STATE.ZOOM; |
| | | break; |
| | | |
| | | case _this.mouseButtons.RIGHT: |
| | | _state = STATE.PAN; |
| | | break; |
| | | |
| | | default: |
| | | _state = STATE.NONE; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var state = (_keyState !== STATE.NONE) ? _keyState : _state; |
| | | |
| | | if (state === STATE.ROTATE && !_this.noRotate) { |
| | | |
| | | _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); |
| | | _movePrev.copy(_moveCurr); |
| | | |
| | | } else if (state === STATE.ZOOM && !_this.noZoom) { |
| | | |
| | | _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY)); |
| | | _zoomEnd.copy(_zoomStart); |
| | | |
| | | } else if (state === STATE.PAN && !_this.noPan) { |
| | | |
| | | _panStart.copy(getMouseOnScreen(event.pageX, event.pageY)); |
| | | _panEnd.copy(_panStart); |
| | | |
| | | } |
| | | |
| | | document.addEventListener('mousemove', mousemove, false); |
| | | document.addEventListener('mouseup', mouseup, false); |
| | | |
| | | _this.dispatchEvent(startEvent); |
| | | |
| | | } |
| | | |
| | | function mousemove(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | var state = (_keyState !== STATE.NONE) ? _keyState : _state; |
| | | |
| | | if (state === STATE.ROTATE && !_this.noRotate) { |
| | | |
| | | _movePrev.copy(_moveCurr); |
| | | _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); |
| | | |
| | | } else if (state === STATE.ZOOM && !_this.noZoom) { |
| | | |
| | | _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); |
| | | |
| | | } else if (state === STATE.PAN && !_this.noPan) { |
| | | |
| | | _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function mouseup(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | _state = STATE.NONE; |
| | | |
| | | document.removeEventListener('mousemove', mousemove); |
| | | document.removeEventListener('mouseup', mouseup); |
| | | _this.dispatchEvent(endEvent); |
| | | |
| | | } |
| | | |
| | | function mousewheel(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | if (_this.noZoom === true) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | switch (event.deltaMode) { |
| | | |
| | | case 2: |
| | | // Zoom in pages |
| | | _zoomStart.y -= event.deltaY * 0.025; |
| | | break; |
| | | |
| | | case 1: |
| | | // Zoom in lines |
| | | _zoomStart.y -= event.deltaY * 0.01; |
| | | break; |
| | | |
| | | default: |
| | | // undefined, 0, assume pixels |
| | | _zoomStart.y -= event.deltaY * 0.00025; |
| | | break; |
| | | |
| | | } |
| | | |
| | | _this.dispatchEvent(startEvent); |
| | | _this.dispatchEvent(endEvent); |
| | | |
| | | } |
| | | |
| | | function touchstart(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | |
| | | switch (event.touches.length) { |
| | | |
| | | case 1: |
| | | _state = STATE.TOUCH_ROTATE; |
| | | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); |
| | | _movePrev.copy(_moveCurr); |
| | | break; |
| | | |
| | | default: // 2 or more |
| | | _state = STATE.TOUCH_ZOOM_PAN; |
| | | var dx = event.touches[0].pageX - event.touches[1].pageX; |
| | | var dy = event.touches[0].pageY - event.touches[1].pageY; |
| | | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy); |
| | | |
| | | var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; |
| | | var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; |
| | | _panStart.copy(getMouseOnScreen(x, y)); |
| | | _panEnd.copy(_panStart); |
| | | break; |
| | | |
| | | } |
| | | |
| | | _this.dispatchEvent(startEvent); |
| | | |
| | | } |
| | | |
| | | function touchmove(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | event.stopPropagation(); |
| | | |
| | | switch (event.touches.length) { |
| | | |
| | | case 1: |
| | | _movePrev.copy(_moveCurr); |
| | | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); |
| | | break; |
| | | |
| | | default: // 2 or more |
| | | var dx = event.touches[0].pageX - event.touches[1].pageX; |
| | | var dy = event.touches[0].pageY - event.touches[1].pageY; |
| | | _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy); |
| | | |
| | | var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; |
| | | var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; |
| | | _panEnd.copy(getMouseOnScreen(x, y)); |
| | | break; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function touchend(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | switch (event.touches.length) { |
| | | |
| | | case 0: |
| | | _state = STATE.NONE; |
| | | break; |
| | | |
| | | case 1: |
| | | _state = STATE.TOUCH_ROTATE; |
| | | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); |
| | | _movePrev.copy(_moveCurr); |
| | | break; |
| | | |
| | | } |
| | | |
| | | _this.dispatchEvent(endEvent); |
| | | |
| | | } |
| | | |
| | | function contextmenu(event) { |
| | | |
| | | if (_this.enabled === false) return; |
| | | |
| | | event.preventDefault(); |
| | | |
| | | } |
| | | |
| | | this.dispose = function () { |
| | | |
| | | this.domElement.removeEventListener('contextmenu', contextmenu, false); |
| | | this.domElement.removeEventListener('mousedown', mousedown, false); |
| | | this.domElement.removeEventListener('wheel', mousewheel, false); |
| | | |
| | | this.domElement.removeEventListener('touchstart', touchstart, false); |
| | | this.domElement.removeEventListener('touchend', touchend, false); |
| | | this.domElement.removeEventListener('touchmove', touchmove, false); |
| | | |
| | | document.removeEventListener('mousemove', mousemove, false); |
| | | document.removeEventListener('mouseup', mouseup, false); |
| | | |
| | | window.removeEventListener('keydown', keydown, false); |
| | | window.removeEventListener('keyup', keyup, false); |
| | | |
| | | }; |
| | | |
| | | this.domElement.addEventListener('contextmenu', contextmenu, false); |
| | | this.domElement.addEventListener('mousedown', mousedown, false); |
| | | this.domElement.addEventListener('wheel', mousewheel, false); |
| | | |
| | | this.domElement.addEventListener('touchstart', touchstart, false); |
| | | this.domElement.addEventListener('touchend', touchend, false); |
| | | this.domElement.addEventListener('touchmove', touchmove, false); |
| | | |
| | | window.addEventListener('keydown', keydown, false); |
| | | window.addEventListener('keyup', keyup, false); |
| | | |
| | | this.handleResize(); |
| | | |
| | | // force an update at start |
| | | this.update(); |
| | | |
| | | }; |
| | | |
| | | TrackballControls.prototype = Object.create(EventDispatcher.prototype); |
| | | TrackballControls.prototype.constructor = TrackballControls; |
| | | |
| | | return { TrackballControls }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author mrdoob / http://mrdoob.com/ |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | CompressedTextureLoader, |
| | | RGBAFormat, |
| | | RGBA_S3TC_DXT3_Format, |
| | | RGBA_S3TC_DXT5_Format, |
| | | RGB_ETC1_Format, |
| | | RGB_S3TC_DXT1_Format |
| | | } = THREE; |
| | | |
| | | var DDSLoader = function (manager) { |
| | | |
| | | CompressedTextureLoader.call(this, manager); |
| | | |
| | | }; |
| | | |
| | | DDSLoader.prototype = Object.assign(Object.create(CompressedTextureLoader.prototype), { |
| | | |
| | | constructor: DDSLoader, |
| | | |
| | | parse: function (buffer, loadMipmaps) { |
| | | |
| | | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 }; |
| | | |
| | | // Adapted from @toji's DDS utils |
| | | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js |
| | | |
| | | // All values and structures referenced from: |
| | | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/ |
| | | |
| | | var DDS_MAGIC = 0x20534444; |
| | | |
| | | var DDSD_CAPS = 0x1, |
| | | DDSD_HEIGHT = 0x2, |
| | | DDSD_WIDTH = 0x4, |
| | | DDSD_PITCH = 0x8, |
| | | DDSD_PIXELFORMAT = 0x1000, |
| | | DDSD_MIPMAPCOUNT = 0x20000, |
| | | DDSD_LINEARSIZE = 0x80000, |
| | | DDSD_DEPTH = 0x800000; |
| | | |
| | | var DDSCAPS_COMPLEX = 0x8, |
| | | DDSCAPS_MIPMAP = 0x400000, |
| | | DDSCAPS_TEXTURE = 0x1000; |
| | | |
| | | var DDSCAPS2_CUBEMAP = 0x200, |
| | | DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, |
| | | DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, |
| | | DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, |
| | | DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, |
| | | DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, |
| | | DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, |
| | | DDSCAPS2_VOLUME = 0x200000; |
| | | |
| | | var DDPF_ALPHAPIXELS = 0x1, |
| | | DDPF_ALPHA = 0x2, |
| | | DDPF_FOURCC = 0x4, |
| | | DDPF_RGB = 0x40, |
| | | DDPF_YUV = 0x200, |
| | | DDPF_LUMINANCE = 0x20000; |
| | | |
| | | function fourCCToInt32(value) { |
| | | |
| | | return value.charCodeAt(0) + |
| | | (value.charCodeAt(1) << 8) + |
| | | (value.charCodeAt(2) << 16) + |
| | | (value.charCodeAt(3) << 24); |
| | | |
| | | } |
| | | |
| | | function int32ToFourCC(value) { |
| | | |
| | | return String.fromCharCode( |
| | | value & 0xff, |
| | | (value >> 8) & 0xff, |
| | | (value >> 16) & 0xff, |
| | | (value >> 24) & 0xff |
| | | ); |
| | | |
| | | } |
| | | |
| | | function loadARGBMip(buffer, dataOffset, width, height) { |
| | | |
| | | var dataLength = width * height * 4; |
| | | var srcBuffer = new Uint8Array(buffer, dataOffset, dataLength); |
| | | var byteArray = new Uint8Array(dataLength); |
| | | var dst = 0; |
| | | var src = 0; |
| | | for (var y = 0; y < height; y++) { |
| | | |
| | | for (var x = 0; x < width; x++) { |
| | | |
| | | var b = srcBuffer[src]; src++; |
| | | var g = srcBuffer[src]; src++; |
| | | var r = srcBuffer[src]; src++; |
| | | var a = srcBuffer[src]; src++; |
| | | byteArray[dst] = r; dst++; //r |
| | | byteArray[dst] = g; dst++; //g |
| | | byteArray[dst] = b; dst++; //b |
| | | byteArray[dst] = a; dst++; //a |
| | | |
| | | } |
| | | |
| | | } |
| | | return byteArray; |
| | | |
| | | } |
| | | |
| | | var FOURCC_DXT1 = fourCCToInt32("DXT1"); |
| | | var FOURCC_DXT3 = fourCCToInt32("DXT3"); |
| | | var FOURCC_DXT5 = fourCCToInt32("DXT5"); |
| | | var FOURCC_ETC1 = fourCCToInt32("ETC1"); |
| | | |
| | | var headerLengthInt = 31; // The header length in 32 bit ints |
| | | |
| | | // Offsets into the header array |
| | | |
| | | var off_magic = 0; |
| | | |
| | | var off_size = 1; |
| | | var off_flags = 2; |
| | | var off_height = 3; |
| | | var off_width = 4; |
| | | |
| | | var off_mipmapCount = 7; |
| | | |
| | | var off_pfFlags = 20; |
| | | var off_pfFourCC = 21; |
| | | var off_RGBBitCount = 22; |
| | | var off_RBitMask = 23; |
| | | var off_GBitMask = 24; |
| | | var off_BBitMask = 25; |
| | | var off_ABitMask = 26; |
| | | |
| | | var off_caps = 27; |
| | | var off_caps2 = 28; |
| | | var off_caps3 = 29; |
| | | var off_caps4 = 30; |
| | | |
| | | // Parse header |
| | | |
| | | var header = new Int32Array(buffer, 0, headerLengthInt); |
| | | |
| | | if (header[off_magic] !== DDS_MAGIC) { |
| | | |
| | | console.error('THREE.DDSLoader.parse: Invalid magic number in DDS header.'); |
| | | return dds; |
| | | |
| | | } |
| | | |
| | | if (!header[off_pfFlags] & DDPF_FOURCC) { |
| | | |
| | | console.error('THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.'); |
| | | return dds; |
| | | |
| | | } |
| | | |
| | | var blockBytes; |
| | | |
| | | var fourCC = header[off_pfFourCC]; |
| | | |
| | | var isRGBAUncompressed = false; |
| | | |
| | | switch (fourCC) { |
| | | |
| | | case FOURCC_DXT1: |
| | | |
| | | blockBytes = 8; |
| | | dds.format = RGB_S3TC_DXT1_Format; |
| | | break; |
| | | |
| | | case FOURCC_DXT3: |
| | | |
| | | blockBytes = 16; |
| | | dds.format = RGBA_S3TC_DXT3_Format; |
| | | break; |
| | | |
| | | case FOURCC_DXT5: |
| | | |
| | | blockBytes = 16; |
| | | dds.format = RGBA_S3TC_DXT5_Format; |
| | | break; |
| | | |
| | | case FOURCC_ETC1: |
| | | |
| | | blockBytes = 8; |
| | | dds.format = RGB_ETC1_Format; |
| | | break; |
| | | |
| | | default: |
| | | |
| | | if (header[off_RGBBitCount] === 32 |
| | | && header[off_RBitMask] & 0xff0000 |
| | | && header[off_GBitMask] & 0xff00 |
| | | && header[off_BBitMask] & 0xff |
| | | && header[off_ABitMask] & 0xff000000) { |
| | | |
| | | isRGBAUncompressed = true; |
| | | blockBytes = 64; |
| | | dds.format = RGBAFormat; |
| | | |
| | | } else { |
| | | |
| | | console.error('THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC(fourCC)); |
| | | return dds; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | dds.mipmapCount = 1; |
| | | |
| | | if (header[off_flags] & DDSD_MIPMAPCOUNT && loadMipmaps !== false) { |
| | | |
| | | dds.mipmapCount = Math.max(1, header[off_mipmapCount]); |
| | | |
| | | } |
| | | |
| | | var caps2 = header[off_caps2]; |
| | | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false; |
| | | if (dds.isCubemap && ( |
| | | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEX) || |
| | | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX) || |
| | | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEY) || |
| | | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY) || |
| | | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ) || |
| | | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ) |
| | | )) { |
| | | |
| | | console.error('THREE.DDSLoader.parse: Incomplete cubemap faces'); |
| | | return dds; |
| | | |
| | | } |
| | | |
| | | dds.width = header[off_width]; |
| | | dds.height = header[off_height]; |
| | | |
| | | var dataOffset = header[off_size] + 4; |
| | | |
| | | // Extract mipmaps buffers |
| | | |
| | | var faces = dds.isCubemap ? 6 : 1; |
| | | |
| | | for (var face = 0; face < faces; face++) { |
| | | |
| | | var width = dds.width; |
| | | var height = dds.height; |
| | | |
| | | for (var i = 0; i < dds.mipmapCount; i++) { |
| | | |
| | | if (isRGBAUncompressed) { |
| | | |
| | | var byteArray = loadARGBMip(buffer, dataOffset, width, height); |
| | | var dataLength = byteArray.length; |
| | | |
| | | } else { |
| | | |
| | | var dataLength = Math.max(4, width) / 4 * Math.max(4, height) / 4 * blockBytes; |
| | | var byteArray = new Uint8Array(buffer, dataOffset, dataLength); |
| | | |
| | | } |
| | | |
| | | var mipmap = { "data": byteArray, "width": width, "height": height }; |
| | | dds.mipmaps.push(mipmap); |
| | | |
| | | dataOffset += dataLength; |
| | | |
| | | width = Math.max(width >> 1, 1); |
| | | height = Math.max(height >> 1, 1); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return dds; |
| | | |
| | | } |
| | | |
| | | }); |
| | | |
| | | return { DDSLoader }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author Richard M. / https://github.com/richardmonette |
| | | * @author WestLangley / http://github.com/WestLangley |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | BackSide, |
| | | BoxBufferGeometry, |
| | | CubeCamera, |
| | | Mesh, |
| | | NoBlending, |
| | | PerspectiveCamera, |
| | | Scene, |
| | | ShaderMaterial, |
| | | UniformsUtils, |
| | | WebGLRenderTargetCube |
| | | } = THREE; |
| | | |
| | | var CubemapGenerator = function ( renderer ) { |
| | | |
| | | this.renderer = renderer; |
| | | |
| | | }; |
| | | |
| | | CubemapGenerator.prototype.fromEquirectangular = function ( texture, options ) { |
| | | |
| | | options = options || {}; |
| | | |
| | | var scene = new Scene(); |
| | | |
| | | var shader = { |
| | | |
| | | uniforms: { |
| | | tEquirect: { value: null }, |
| | | }, |
| | | |
| | | vertexShader: |
| | | |
| | | ` |
| | | varying vec3 vWorldDirection; |
| | | |
| | | //include <common> |
| | | vec3 transformDirection( in vec3 dir, in mat4 matrix ) { |
| | | |
| | | return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); |
| | | |
| | | } |
| | | |
| | | void main() { |
| | | |
| | | vWorldDirection = transformDirection( position, modelMatrix ); |
| | | |
| | | #include <begin_vertex> |
| | | #include <project_vertex> |
| | | |
| | | } |
| | | `, |
| | | |
| | | fragmentShader: |
| | | |
| | | ` |
| | | uniform sampler2D tEquirect; |
| | | |
| | | varying vec3 vWorldDirection; |
| | | |
| | | //include <common> |
| | | #define RECIPROCAL_PI 0.31830988618 |
| | | #define RECIPROCAL_PI2 0.15915494 |
| | | |
| | | void main() { |
| | | |
| | | vec3 direction = normalize( vWorldDirection ); |
| | | |
| | | vec2 sampleUV; |
| | | |
| | | sampleUV.y = asin( clamp( direction.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; |
| | | |
| | | sampleUV.x = atan( direction.z, direction.x ) * RECIPROCAL_PI2 + 0.5; |
| | | |
| | | gl_FragColor = texture2D( tEquirect, sampleUV ); |
| | | |
| | | } |
| | | ` |
| | | }; |
| | | |
| | | var material = new ShaderMaterial( { |
| | | |
| | | type: 'CubemapFromEquirect', |
| | | |
| | | uniforms: UniformsUtils.clone( shader.uniforms ), |
| | | vertexShader: shader.vertexShader, |
| | | fragmentShader: shader.fragmentShader, |
| | | side: BackSide, |
| | | blending: NoBlending |
| | | |
| | | } ); |
| | | |
| | | material.uniforms.tEquirect.value = texture; |
| | | |
| | | var mesh = new Mesh( new BoxBufferGeometry( 5, 5, 5 ), material ); |
| | | |
| | | scene.add( mesh ); |
| | | |
| | | var resolution = options.resolution || 512; |
| | | |
| | | var params = { |
| | | type: texture.type, |
| | | format: texture.format, |
| | | encoding: texture.encoding, |
| | | generateMipmaps: ( options.generateMipmaps !== undefined ) ? options.generateMipmaps : texture.generateMipmaps, |
| | | minFilter: ( options.minFilter !== undefined ) ? options.minFilter : texture.minFilter, |
| | | magFilter: ( options.magFilter !== undefined ) ? options.magFilter : texture.magFilter |
| | | }; |
| | | |
| | | var camera = new CubeCamera( 1, 10, resolution, params ); |
| | | |
| | | camera.update( this.renderer, scene ); |
| | | |
| | | mesh.geometry.dispose(); |
| | | mesh.material.dispose(); |
| | | |
| | | return camera.renderTarget; |
| | | |
| | | }; |
| | | |
| | | // |
| | | |
| | | var EquirectangularToCubeGenerator = ( function () { |
| | | |
| | | var camera = new PerspectiveCamera( 90, 1, 0.1, 10 ); |
| | | var scene = new Scene(); |
| | | var boxMesh = new Mesh( new BoxBufferGeometry( 1, 1, 1 ), getShader() ); |
| | | boxMesh.material.side = BackSide; |
| | | scene.add( boxMesh ); |
| | | |
| | | var EquirectangularToCubeGenerator = function ( sourceTexture, options ) { |
| | | |
| | | options = options || {}; |
| | | |
| | | this.sourceTexture = sourceTexture; |
| | | this.resolution = options.resolution || 512; |
| | | |
| | | this.views = [ |
| | | { t: [ 1, 0, 0 ], u: [ 0, - 1, 0 ] }, |
| | | { t: [ - 1, 0, 0 ], u: [ 0, - 1, 0 ] }, |
| | | { t: [ 0, 1, 0 ], u: [ 0, 0, 1 ] }, |
| | | { t: [ 0, - 1, 0 ], u: [ 0, 0, - 1 ] }, |
| | | { t: [ 0, 0, 1 ], u: [ 0, - 1, 0 ] }, |
| | | { t: [ 0, 0, - 1 ], u: [ 0, - 1, 0 ] }, |
| | | ]; |
| | | |
| | | var params = { |
| | | format: options.format || this.sourceTexture.format, |
| | | magFilter: this.sourceTexture.magFilter, |
| | | minFilter: this.sourceTexture.minFilter, |
| | | type: options.type || this.sourceTexture.type, |
| | | generateMipmaps: this.sourceTexture.generateMipmaps, |
| | | anisotropy: this.sourceTexture.anisotropy, |
| | | encoding: this.sourceTexture.encoding |
| | | }; |
| | | |
| | | this.renderTarget = new WebGLRenderTargetCube( this.resolution, this.resolution, params ); |
| | | |
| | | }; |
| | | |
| | | EquirectangularToCubeGenerator.prototype = { |
| | | |
| | | constructor: EquirectangularToCubeGenerator, |
| | | |
| | | update: function ( renderer ) { |
| | | |
| | | var currentRenderTarget = renderer.getRenderTarget(); |
| | | |
| | | boxMesh.material.uniforms.equirectangularMap.value = this.sourceTexture; |
| | | |
| | | for ( var i = 0; i < 6; i ++ ) { |
| | | |
| | | var v = this.views[ i ]; |
| | | |
| | | camera.position.set( 0, 0, 0 ); |
| | | camera.up.set( v.u[ 0 ], v.u[ 1 ], v.u[ 2 ] ); |
| | | camera.lookAt( v.t[ 0 ], v.t[ 1 ], v.t[ 2 ] ); |
| | | |
| | | renderer.setRenderTarget( this.renderTarget, i ); |
| | | renderer.clear(); |
| | | renderer.render( scene, camera ); |
| | | |
| | | } |
| | | |
| | | renderer.setRenderTarget( currentRenderTarget ); |
| | | |
| | | return this.renderTarget.texture; |
| | | |
| | | }, |
| | | |
| | | dispose: function () { |
| | | |
| | | this.renderTarget.dispose(); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | function getShader() { |
| | | |
| | | var shaderMaterial = new ShaderMaterial( { |
| | | |
| | | uniforms: { |
| | | "equirectangularMap": { value: null }, |
| | | }, |
| | | |
| | | vertexShader: |
| | | "varying vec3 localPosition;\n\ |
| | | \n\ |
| | | void main() {\n\ |
| | | localPosition = position;\n\ |
| | | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\ |
| | | }", |
| | | |
| | | fragmentShader: |
| | | "#include <common>\n\ |
| | | varying vec3 localPosition;\n\ |
| | | uniform sampler2D equirectangularMap;\n\ |
| | | \n\ |
| | | vec2 EquirectangularSampleUV(vec3 v) {\n\ |
| | | vec2 uv = vec2(atan(v.z, v.x), asin(v.y));\n\ |
| | | uv *= vec2(0.1591, 0.3183); // inverse atan\n\ |
| | | uv += 0.5;\n\ |
| | | return uv;\n\ |
| | | }\n\ |
| | | \n\ |
| | | void main() {\n\ |
| | | vec2 uv = EquirectangularSampleUV(normalize(localPosition));\n\ |
| | | gl_FragColor = texture2D(equirectangularMap, uv);\n\ |
| | | }", |
| | | |
| | | blending: NoBlending |
| | | |
| | | } ); |
| | | |
| | | shaderMaterial.type = 'EquirectangularToCubeGenerator'; |
| | | |
| | | return shaderMaterial; |
| | | |
| | | } |
| | | |
| | | return EquirectangularToCubeGenerator; |
| | | |
| | | } )(); |
| | | |
| | | return { CubemapGenerator, EquirectangularToCubeGenerator }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author Rich Tibbett / https://github.com/richtr |
| | | * @author mrdoob / http://mrdoob.com/ |
| | | * @author Tony Parisi / http://www.tonyparisi.com/ |
| | | * @author Takahiro / https://github.com/takahirox |
| | | * @author Don McCurdy / https://www.donmccurdy.com |
| | | */ |
| | | |
| | | |
| | | export default function (THREE) { |
| | | let { |
| | | AnimationClip, |
| | | Bone, |
| | | BufferAttribute, |
| | | BufferGeometry, |
| | | ClampToEdgeWrapping, |
| | | Color, |
| | | DefaultLoadingManager, |
| | | DirectionalLight, |
| | | DoubleSide, |
| | | FileLoader, |
| | | FrontSide, |
| | | Group, |
| | | InterleavedBuffer, |
| | | InterleavedBufferAttribute, |
| | | Interpolant, |
| | | InterpolateDiscrete, |
| | | InterpolateLinear, |
| | | Line, |
| | | LineBasicMaterial, |
| | | LineLoop, |
| | | LineSegments, |
| | | LinearFilter, |
| | | LinearMipmapLinearFilter, |
| | | LinearMipmapNearestFilter, |
| | | Loader, |
| | | LoaderUtils, |
| | | Material, |
| | | Math: _Math, |
| | | Matrix4, |
| | | Mesh, |
| | | MeshBasicMaterial, |
| | | MeshStandardMaterial, |
| | | MirroredRepeatWrapping, |
| | | NearestFilter, |
| | | NearestMipmapLinearFilter, |
| | | NearestMipmapNearestFilter, |
| | | NumberKeyframeTrack, |
| | | Object3D, |
| | | OrthographicCamera, |
| | | PerspectiveCamera, |
| | | PointLight, |
| | | Points, |
| | | PointsMaterial, |
| | | PropertyBinding, |
| | | QuaternionKeyframeTrack, |
| | | RGBAFormat, |
| | | RGBFormat, |
| | | RepeatWrapping, |
| | | Scene, |
| | | ShaderLib, |
| | | ShaderMaterial, |
| | | Skeleton, |
| | | SkinnedMesh, |
| | | SpotLight, |
| | | TextureLoader, |
| | | TriangleFanDrawMode, |
| | | TriangleStripDrawMode, |
| | | UniformsUtils, |
| | | Vector2, |
| | | VectorKeyframeTrack, |
| | | VertexColors, |
| | | sRGBEncoding, |
| | | global: window |
| | | } = THREE; |
| | | |
| | | function GLTFLoader(manager) { |
| | | |
| | | this.manager = (manager !== undefined) ? manager : DefaultLoadingManager; |
| | | this.dracoLoader = null; |
| | | this.ddsLoader = null; |
| | | |
| | | } |
| | | |
| | | GLTFLoader.prototype = { |
| | | |
| | | constructor: GLTFLoader, |
| | | |
| | | crossOrigin: 'anonymous', |
| | | |
| | | load: function (url, onLoad, onProgress, onError) { |
| | | |
| | | var scope = this; |
| | | |
| | | var resourcePath; |
| | | |
| | | if (this.resourcePath !== undefined) { |
| | | |
| | | resourcePath = this.resourcePath; |
| | | |
| | | } else if (this.path !== undefined) { |
| | | |
| | | resourcePath = this.path; |
| | | |
| | | } else { |
| | | |
| | | resourcePath = LoaderUtils.extractUrlBase(url); |
| | | |
| | | } |
| | | |
| | | // Tells the LoadingManager to track an extra item, which resolves after |
| | | // the model is fully loaded. This means the count of items loaded will |
| | | // be incorrect, but ensures manager.onLoad() does not fire early. |
| | | scope.manager.itemStart(url); |
| | | |
| | | var _onError = function (e) { |
| | | |
| | | if (onError) { |
| | | |
| | | onError(e); |
| | | |
| | | } else { |
| | | |
| | | console.error(e); |
| | | |
| | | } |
| | | |
| | | scope.manager.itemError(url); |
| | | scope.manager.itemEnd(url); |
| | | |
| | | }; |
| | | |
| | | var loader = new FileLoader(scope.manager); |
| | | |
| | | loader.setPath(this.path); |
| | | loader.setResponseType('arraybuffer'); |
| | | |
| | | if (scope.crossOrigin === 'use-credentials') { |
| | | |
| | | loader.setWithCredentials(true); |
| | | |
| | | } |
| | | |
| | | loader.load(url, function (data) { |
| | | |
| | | try { |
| | | |
| | | scope.parse(data, resourcePath, function (gltf) { |
| | | |
| | | onLoad(gltf); |
| | | |
| | | scope.manager.itemEnd(url); |
| | | |
| | | }, _onError); |
| | | |
| | | } catch (e) { |
| | | |
| | | _onError(e); |
| | | |
| | | } |
| | | |
| | | }, onProgress, _onError); |
| | | |
| | | }, |
| | | |
| | | setCrossOrigin: function (value) { |
| | | |
| | | this.crossOrigin = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | setPath: function (value) { |
| | | |
| | | this.path = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | setResourcePath: function (value) { |
| | | |
| | | this.resourcePath = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | setDRACOLoader: function (dracoLoader) { |
| | | |
| | | this.dracoLoader = dracoLoader; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | setDDSLoader: function (ddsLoader) { |
| | | |
| | | this.ddsLoader = ddsLoader; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | parse: function (data, path, onLoad, onError) { |
| | | |
| | | var content; |
| | | var extensions = {}; |
| | | |
| | | if (typeof data === 'string') { |
| | | |
| | | content = data; |
| | | |
| | | } else { |
| | | |
| | | var magic = LoaderUtils.decodeText(new Uint8Array(data, 0, 4)); |
| | | |
| | | if (magic === BINARY_EXTENSION_HEADER_MAGIC) { |
| | | |
| | | try { |
| | | |
| | | extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data); |
| | | |
| | | } catch (error) { |
| | | |
| | | if (onError) onError(error); |
| | | return; |
| | | |
| | | } |
| | | |
| | | content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content; |
| | | |
| | | } else { |
| | | |
| | | content = LoaderUtils.decodeText(new Uint8Array(data)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var json = JSON.parse(content); |
| | | |
| | | if (json.asset === undefined || json.asset.version[0] < 2) { |
| | | |
| | | if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.')); |
| | | return; |
| | | |
| | | } |
| | | |
| | | if (json.extensionsUsed) { |
| | | |
| | | for (var i = 0; i < json.extensionsUsed.length; ++i) { |
| | | |
| | | var extensionName = json.extensionsUsed[i]; |
| | | var extensionsRequired = json.extensionsRequired || []; |
| | | |
| | | switch (extensionName) { |
| | | |
| | | case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: |
| | | extensions[extensionName] = new GLTFLightsExtension(json); |
| | | break; |
| | | |
| | | case EXTENSIONS.KHR_MATERIALS_UNLIT: |
| | | extensions[extensionName] = new GLTFMaterialsUnlitExtension(); |
| | | break; |
| | | |
| | | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: |
| | | extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension(); |
| | | break; |
| | | |
| | | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: |
| | | extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader); |
| | | break; |
| | | |
| | | case EXTENSIONS.MSFT_TEXTURE_DDS: |
| | | extensions[EXTENSIONS.MSFT_TEXTURE_DDS] = new GLTFTextureDDSExtension(this.ddsLoader); |
| | | break; |
| | | |
| | | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: |
| | | extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] = new GLTFTextureTransformExtension(); |
| | | break; |
| | | |
| | | default: |
| | | |
| | | if (extensionsRequired.indexOf(extensionName) >= 0) { |
| | | |
| | | console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var parser = new GLTFParser(json, extensions, { |
| | | |
| | | path: path || this.resourcePath || '', |
| | | crossOrigin: this.crossOrigin, |
| | | manager: this.manager |
| | | |
| | | }); |
| | | |
| | | parser.parse(onLoad, onError); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | /* GLTFREGISTRY */ |
| | | |
| | | function GLTFRegistry() { |
| | | |
| | | var objects = {}; |
| | | |
| | | return { |
| | | |
| | | get: function (key) { |
| | | |
| | | return objects[key]; |
| | | |
| | | }, |
| | | |
| | | add: function (key, object) { |
| | | |
| | | objects[key] = object; |
| | | |
| | | }, |
| | | |
| | | remove: function (key) { |
| | | |
| | | delete objects[key]; |
| | | |
| | | }, |
| | | |
| | | removeAll: function () { |
| | | |
| | | objects = {}; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | } |
| | | |
| | | /*********************************/ |
| | | /********** EXTENSIONS ***********/ |
| | | /*********************************/ |
| | | |
| | | var EXTENSIONS = { |
| | | KHR_BINARY_GLTF: 'KHR_binary_glTF', |
| | | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', |
| | | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', |
| | | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', |
| | | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', |
| | | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', |
| | | MSFT_TEXTURE_DDS: 'MSFT_texture_dds' |
| | | }; |
| | | |
| | | /** |
| | | * DDS Texture Extension |
| | | * |
| | | * Specification: |
| | | * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds |
| | | * |
| | | */ |
| | | function GLTFTextureDDSExtension(ddsLoader) { |
| | | |
| | | if (!ddsLoader) { |
| | | |
| | | throw new Error('THREE.GLTFLoader: Attempting to load .dds texture without importing DDSLoader'); |
| | | |
| | | } |
| | | |
| | | this.name = EXTENSIONS.MSFT_TEXTURE_DDS; |
| | | this.ddsLoader = ddsLoader; |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Lights Extension |
| | | * |
| | | * Specification: PENDING |
| | | */ |
| | | function GLTFLightsExtension(json) { |
| | | |
| | | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; |
| | | |
| | | var extension = (json.extensions && json.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL]) || {}; |
| | | this.lightDefs = extension.lights || []; |
| | | |
| | | } |
| | | |
| | | GLTFLightsExtension.prototype.loadLight = function (lightIndex) { |
| | | |
| | | var lightDef = this.lightDefs[lightIndex]; |
| | | var lightNode; |
| | | |
| | | var color = new Color(0xffffff); |
| | | if (lightDef.color !== undefined) color.fromArray(lightDef.color); |
| | | |
| | | var range = lightDef.range !== undefined ? lightDef.range : 0; |
| | | |
| | | switch (lightDef.type) { |
| | | |
| | | case 'directional': |
| | | lightNode = new DirectionalLight(color); |
| | | lightNode.target.position.set(0, 0, - 1); |
| | | lightNode.add(lightNode.target); |
| | | break; |
| | | |
| | | case 'point': |
| | | lightNode = new PointLight(color); |
| | | lightNode.distance = range; |
| | | break; |
| | | |
| | | case 'spot': |
| | | lightNode = new SpotLight(color); |
| | | lightNode.distance = range; |
| | | // Handle spotlight properties. |
| | | lightDef.spot = lightDef.spot || {}; |
| | | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; |
| | | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; |
| | | lightNode.angle = lightDef.spot.outerConeAngle; |
| | | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; |
| | | lightNode.target.position.set(0, 0, - 1); |
| | | lightNode.add(lightNode.target); |
| | | break; |
| | | |
| | | default: |
| | | throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".'); |
| | | |
| | | } |
| | | |
| | | // Some lights (e.g. spot) default to a position other than the origin. Reset the position |
| | | // here, because node-level parsing will only override position if explicitly specified. |
| | | lightNode.position.set(0, 0, 0); |
| | | |
| | | lightNode.decay = 2; |
| | | |
| | | if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity; |
| | | |
| | | lightNode.name = lightDef.name || ('light_' + lightIndex); |
| | | |
| | | return Promise.resolve(lightNode); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Unlit Materials Extension (pending) |
| | | * |
| | | * PR: https://github.com/KhronosGroup/glTF/pull/1163 |
| | | */ |
| | | function GLTFMaterialsUnlitExtension() { |
| | | |
| | | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; |
| | | |
| | | } |
| | | |
| | | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { |
| | | |
| | | return MeshBasicMaterial; |
| | | |
| | | }; |
| | | |
| | | GLTFMaterialsUnlitExtension.prototype.extendParams = function (materialParams, materialDef, parser) { |
| | | |
| | | var pending = []; |
| | | |
| | | materialParams.color = new Color(1.0, 1.0, 1.0); |
| | | materialParams.opacity = 1.0; |
| | | |
| | | var metallicRoughness = materialDef.pbrMetallicRoughness; |
| | | |
| | | if (metallicRoughness) { |
| | | |
| | | if (Array.isArray(metallicRoughness.baseColorFactor)) { |
| | | |
| | | var array = metallicRoughness.baseColorFactor; |
| | | |
| | | materialParams.color.fromArray(array); |
| | | materialParams.opacity = array[3]; |
| | | |
| | | } |
| | | |
| | | if (metallicRoughness.baseColorTexture !== undefined) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending); |
| | | |
| | | }; |
| | | |
| | | /* BINARY EXTENSION */ |
| | | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; |
| | | var BINARY_EXTENSION_HEADER_LENGTH = 12; |
| | | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; |
| | | |
| | | function GLTFBinaryExtension(data) { |
| | | |
| | | this.name = EXTENSIONS.KHR_BINARY_GLTF; |
| | | this.content = null; |
| | | this.body = null; |
| | | |
| | | var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH); |
| | | |
| | | this.header = { |
| | | magic: LoaderUtils.decodeText(new Uint8Array(data.slice(0, 4))), |
| | | version: headerView.getUint32(4, true), |
| | | length: headerView.getUint32(8, true) |
| | | }; |
| | | |
| | | if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) { |
| | | |
| | | throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.'); |
| | | |
| | | } else if (this.header.version < 2.0) { |
| | | |
| | | throw new Error('THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.'); |
| | | |
| | | } |
| | | |
| | | var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH); |
| | | var chunkIndex = 0; |
| | | |
| | | while (chunkIndex < chunkView.byteLength) { |
| | | |
| | | var chunkLength = chunkView.getUint32(chunkIndex, true); |
| | | chunkIndex += 4; |
| | | |
| | | var chunkType = chunkView.getUint32(chunkIndex, true); |
| | | chunkIndex += 4; |
| | | |
| | | if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) { |
| | | |
| | | var contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength); |
| | | this.content = LoaderUtils.decodeText(contentArray); |
| | | |
| | | } else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) { |
| | | |
| | | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; |
| | | this.body = data.slice(byteOffset, byteOffset + chunkLength); |
| | | |
| | | } |
| | | |
| | | // Clients must ignore chunks with unknown types. |
| | | |
| | | chunkIndex += chunkLength; |
| | | |
| | | } |
| | | |
| | | if (this.content === null) { |
| | | |
| | | throw new Error('THREE.GLTFLoader: JSON content not found.'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * DRACO Mesh Compression Extension |
| | | * |
| | | * Specification: https://github.com/KhronosGroup/glTF/pull/874 |
| | | */ |
| | | function GLTFDracoMeshCompressionExtension(json, dracoLoader) { |
| | | |
| | | if (!dracoLoader) { |
| | | |
| | | throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.'); |
| | | |
| | | } |
| | | |
| | | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; |
| | | this.json = json; |
| | | this.dracoLoader = dracoLoader; |
| | | |
| | | } |
| | | |
| | | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function (primitive, parser) { |
| | | |
| | | var json = this.json; |
| | | var dracoLoader = this.dracoLoader; |
| | | var bufferViewIndex = primitive.extensions[this.name].bufferView; |
| | | var gltfAttributeMap = primitive.extensions[this.name].attributes; |
| | | var threeAttributeMap = {}; |
| | | var attributeNormalizedMap = {}; |
| | | var attributeTypeMap = {}; |
| | | |
| | | for (var attributeName in gltfAttributeMap) { |
| | | |
| | | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); |
| | | |
| | | threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName]; |
| | | |
| | | } |
| | | |
| | | for (attributeName in primitive.attributes) { |
| | | |
| | | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); |
| | | |
| | | if (gltfAttributeMap[attributeName] !== undefined) { |
| | | |
| | | var accessorDef = json.accessors[primitive.attributes[attributeName]]; |
| | | var componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; |
| | | |
| | | attributeTypeMap[threeAttributeName] = componentType; |
| | | attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) { |
| | | |
| | | return new Promise(function (resolve) { |
| | | |
| | | dracoLoader.decodeDracoFile(bufferView, function (geometry) { |
| | | |
| | | for (var attributeName in geometry.attributes) { |
| | | |
| | | var attribute = geometry.attributes[attributeName]; |
| | | var normalized = attributeNormalizedMap[attributeName]; |
| | | |
| | | if (normalized !== undefined) attribute.normalized = normalized; |
| | | |
| | | } |
| | | |
| | | resolve(geometry); |
| | | |
| | | }, threeAttributeMap, attributeTypeMap); |
| | | |
| | | }); |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Texture Transform Extension |
| | | * |
| | | * Specification: |
| | | */ |
| | | function GLTFTextureTransformExtension() { |
| | | |
| | | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; |
| | | |
| | | } |
| | | |
| | | GLTFTextureTransformExtension.prototype.extendTexture = function (texture, transform) { |
| | | |
| | | texture = texture.clone(); |
| | | |
| | | if (transform.offset !== undefined) { |
| | | |
| | | texture.offset.fromArray(transform.offset); |
| | | |
| | | } |
| | | |
| | | if (transform.rotation !== undefined) { |
| | | |
| | | texture.rotation = transform.rotation; |
| | | |
| | | } |
| | | |
| | | if (transform.scale !== undefined) { |
| | | |
| | | texture.repeat.fromArray(transform.scale); |
| | | |
| | | } |
| | | |
| | | if (transform.texCoord !== undefined) { |
| | | |
| | | console.warn('THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.'); |
| | | |
| | | } |
| | | |
| | | texture.needsUpdate = true; |
| | | |
| | | return texture; |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specular-Glossiness Extension |
| | | * |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness |
| | | */ |
| | | function GLTFMaterialsPbrSpecularGlossinessExtension() { |
| | | |
| | | return { |
| | | |
| | | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, |
| | | |
| | | specularGlossinessParams: [ |
| | | 'color', |
| | | 'map', |
| | | 'lightMap', |
| | | 'lightMapIntensity', |
| | | 'aoMap', |
| | | 'aoMapIntensity', |
| | | 'emissive', |
| | | 'emissiveIntensity', |
| | | 'emissiveMap', |
| | | 'bumpMap', |
| | | 'bumpScale', |
| | | 'normalMap', |
| | | 'displacementMap', |
| | | 'displacementScale', |
| | | 'displacementBias', |
| | | 'specularMap', |
| | | 'specular', |
| | | 'glossinessMap', |
| | | 'glossiness', |
| | | 'alphaMap', |
| | | 'envMap', |
| | | 'envMapIntensity', |
| | | 'refractionRatio', |
| | | ], |
| | | |
| | | getMaterialType: function () { |
| | | |
| | | return ShaderMaterial; |
| | | |
| | | }, |
| | | |
| | | extendParams: function (materialParams, materialDef, parser) { |
| | | |
| | | var pbrSpecularGlossiness = materialDef.extensions[this.name]; |
| | | |
| | | var shader = ShaderLib['standard']; |
| | | |
| | | var uniforms = UniformsUtils.clone(shader.uniforms); |
| | | |
| | | var specularMapParsFragmentChunk = [ |
| | | '#ifdef USE_SPECULARMAP', |
| | | ' uniform sampler2D specularMap;', |
| | | '#endif' |
| | | ].join('\n'); |
| | | |
| | | var glossinessMapParsFragmentChunk = [ |
| | | '#ifdef USE_GLOSSINESSMAP', |
| | | ' uniform sampler2D glossinessMap;', |
| | | '#endif' |
| | | ].join('\n'); |
| | | |
| | | var specularMapFragmentChunk = [ |
| | | 'vec3 specularFactor = specular;', |
| | | '#ifdef USE_SPECULARMAP', |
| | | ' vec4 texelSpecular = texture2D( specularMap, vUv );', |
| | | ' texelSpecular = sRGBToLinear( texelSpecular );', |
| | | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', |
| | | ' specularFactor *= texelSpecular.rgb;', |
| | | '#endif' |
| | | ].join('\n'); |
| | | |
| | | var glossinessMapFragmentChunk = [ |
| | | 'float glossinessFactor = glossiness;', |
| | | '#ifdef USE_GLOSSINESSMAP', |
| | | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', |
| | | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', |
| | | ' glossinessFactor *= texelGlossiness.a;', |
| | | '#endif' |
| | | ].join('\n'); |
| | | |
| | | var lightPhysicalFragmentChunk = [ |
| | | 'PhysicalMaterial material;', |
| | | 'material.diffuseColor = diffuseColor.rgb;', |
| | | 'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );', |
| | | 'material.specularColor = specularFactor.rgb;', |
| | | ].join('\n'); |
| | | |
| | | var fragmentShader = shader.fragmentShader |
| | | .replace('uniform float roughness;', 'uniform vec3 specular;') |
| | | .replace('uniform float metalness;', 'uniform float glossiness;') |
| | | .replace('#include <roughnessmap_pars_fragment>', specularMapParsFragmentChunk) |
| | | .replace('#include <metalnessmap_pars_fragment>', glossinessMapParsFragmentChunk) |
| | | .replace('#include <roughnessmap_fragment>', specularMapFragmentChunk) |
| | | .replace('#include <metalnessmap_fragment>', glossinessMapFragmentChunk) |
| | | .replace('#include <lights_physical_fragment>', lightPhysicalFragmentChunk); |
| | | |
| | | delete uniforms.roughness; |
| | | delete uniforms.metalness; |
| | | delete uniforms.roughnessMap; |
| | | delete uniforms.metalnessMap; |
| | | |
| | | uniforms.specular = { value: new Color().setHex(0x111111) }; |
| | | uniforms.glossiness = { value: 0.5 }; |
| | | uniforms.specularMap = { value: null }; |
| | | uniforms.glossinessMap = { value: null }; |
| | | |
| | | materialParams.vertexShader = shader.vertexShader; |
| | | materialParams.fragmentShader = fragmentShader; |
| | | materialParams.uniforms = uniforms; |
| | | materialParams.defines = { 'STANDARD': '' } |
| | | |
| | | materialParams.color = new Color(1.0, 1.0, 1.0); |
| | | materialParams.opacity = 1.0; |
| | | |
| | | var pending = []; |
| | | |
| | | if (Array.isArray(pbrSpecularGlossiness.diffuseFactor)) { |
| | | |
| | | var array = pbrSpecularGlossiness.diffuseFactor; |
| | | |
| | | materialParams.color.fromArray(array); |
| | | materialParams.opacity = array[3]; |
| | | |
| | | } |
| | | |
| | | if (pbrSpecularGlossiness.diffuseTexture !== undefined) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'map', pbrSpecularGlossiness.diffuseTexture)); |
| | | |
| | | } |
| | | |
| | | materialParams.emissive = new Color(0.0, 0.0, 0.0); |
| | | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; |
| | | materialParams.specular = new Color(1.0, 1.0, 1.0); |
| | | |
| | | if (Array.isArray(pbrSpecularGlossiness.specularFactor)) { |
| | | |
| | | materialParams.specular.fromArray(pbrSpecularGlossiness.specularFactor); |
| | | |
| | | } |
| | | |
| | | if (pbrSpecularGlossiness.specularGlossinessTexture !== undefined) { |
| | | |
| | | var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; |
| | | pending.push(parser.assignTexture(materialParams, 'glossinessMap', specGlossMapDef)); |
| | | pending.push(parser.assignTexture(materialParams, 'specularMap', specGlossMapDef)); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending); |
| | | |
| | | }, |
| | | |
| | | createMaterial: function (params) { |
| | | |
| | | // setup material properties based on MeshStandardMaterial for Specular-Glossiness |
| | | |
| | | var material = new ShaderMaterial({ |
| | | defines: params.defines, |
| | | vertexShader: params.vertexShader, |
| | | fragmentShader: params.fragmentShader, |
| | | uniforms: params.uniforms, |
| | | fog: true, |
| | | lights: true, |
| | | opacity: params.opacity, |
| | | transparent: params.transparent |
| | | }); |
| | | |
| | | material.isGLTFSpecularGlossinessMaterial = true; |
| | | |
| | | material.color = params.color; |
| | | |
| | | material.map = params.map === undefined ? null : params.map; |
| | | |
| | | material.lightMap = null; |
| | | material.lightMapIntensity = 1.0; |
| | | |
| | | material.aoMap = params.aoMap === undefined ? null : params.aoMap; |
| | | material.aoMapIntensity = 1.0; |
| | | |
| | | material.emissive = params.emissive; |
| | | material.emissiveIntensity = 1.0; |
| | | material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap; |
| | | |
| | | material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap; |
| | | material.bumpScale = 1; |
| | | |
| | | material.normalMap = params.normalMap === undefined ? null : params.normalMap; |
| | | |
| | | if (params.normalScale) material.normalScale = params.normalScale; |
| | | |
| | | material.displacementMap = null; |
| | | material.displacementScale = 1; |
| | | material.displacementBias = 0; |
| | | |
| | | material.specularMap = params.specularMap === undefined ? null : params.specularMap; |
| | | material.specular = params.specular; |
| | | |
| | | material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap; |
| | | material.glossiness = params.glossiness; |
| | | |
| | | material.alphaMap = null; |
| | | |
| | | material.envMap = params.envMap === undefined ? null : params.envMap; |
| | | material.envMapIntensity = 1.0; |
| | | |
| | | material.refractionRatio = 0.98; |
| | | |
| | | material.extensions.derivatives = true; |
| | | |
| | | return material; |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can |
| | | * copy only properties it knows about or inherits, and misses many properties that would |
| | | * normally be defined by MeshStandardMaterial. |
| | | * |
| | | * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of |
| | | * loading a glTF model, but cloning later (e.g. by the user) would require these changes |
| | | * AND also updating `.onBeforeRender` on the parent mesh. |
| | | * |
| | | * @param {ShaderMaterial} source |
| | | * @return {ShaderMaterial} |
| | | */ |
| | | cloneMaterial: function (source) { |
| | | |
| | | var target = source.clone(); |
| | | |
| | | target.isGLTFSpecularGlossinessMaterial = true; |
| | | |
| | | var params = this.specularGlossinessParams; |
| | | |
| | | for (var i = 0, il = params.length; i < il; i++) { |
| | | |
| | | var value = source[params[i]]; |
| | | target[params[i]] = (value && value.isColor) ? value.clone() : value; |
| | | |
| | | } |
| | | |
| | | return target; |
| | | |
| | | }, |
| | | |
| | | // Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer. |
| | | refreshUniforms: function (renderer, scene, camera, geometry, material) { |
| | | |
| | | if (material.isGLTFSpecularGlossinessMaterial !== true) { |
| | | |
| | | return; |
| | | |
| | | } |
| | | |
| | | var uniforms = material.uniforms; |
| | | var defines = material.defines; |
| | | |
| | | uniforms.opacity.value = material.opacity; |
| | | |
| | | uniforms.diffuse.value.copy(material.color); |
| | | uniforms.emissive.value.copy(material.emissive).multiplyScalar(material.emissiveIntensity); |
| | | |
| | | uniforms.map.value = material.map; |
| | | uniforms.specularMap.value = material.specularMap; |
| | | uniforms.alphaMap.value = material.alphaMap; |
| | | |
| | | uniforms.lightMap.value = material.lightMap; |
| | | uniforms.lightMapIntensity.value = material.lightMapIntensity; |
| | | |
| | | uniforms.aoMap.value = material.aoMap; |
| | | uniforms.aoMapIntensity.value = material.aoMapIntensity; |
| | | |
| | | // uv repeat and offset setting priorities |
| | | // 1. color map |
| | | // 2. specular map |
| | | // 3. normal map |
| | | // 4. bump map |
| | | // 5. alpha map |
| | | // 6. emissive map |
| | | |
| | | var uvScaleMap; |
| | | |
| | | if (material.map) { |
| | | |
| | | uvScaleMap = material.map; |
| | | |
| | | } else if (material.specularMap) { |
| | | |
| | | uvScaleMap = material.specularMap; |
| | | |
| | | } else if (material.displacementMap) { |
| | | |
| | | uvScaleMap = material.displacementMap; |
| | | |
| | | } else if (material.normalMap) { |
| | | |
| | | uvScaleMap = material.normalMap; |
| | | |
| | | } else if (material.bumpMap) { |
| | | |
| | | uvScaleMap = material.bumpMap; |
| | | |
| | | } else if (material.glossinessMap) { |
| | | |
| | | uvScaleMap = material.glossinessMap; |
| | | |
| | | } else if (material.alphaMap) { |
| | | |
| | | uvScaleMap = material.alphaMap; |
| | | |
| | | } else if (material.emissiveMap) { |
| | | |
| | | uvScaleMap = material.emissiveMap; |
| | | |
| | | } |
| | | |
| | | if (uvScaleMap !== undefined) { |
| | | |
| | | // backwards compatibility |
| | | if (uvScaleMap.isWebGLRenderTarget) { |
| | | |
| | | uvScaleMap = uvScaleMap.texture; |
| | | |
| | | } |
| | | |
| | | if (uvScaleMap.matrixAutoUpdate === true) { |
| | | |
| | | uvScaleMap.updateMatrix(); |
| | | |
| | | } |
| | | |
| | | uniforms.uvTransform.value.copy(uvScaleMap.matrix); |
| | | |
| | | } |
| | | |
| | | if (material.envMap) { |
| | | |
| | | uniforms.envMap.value = material.envMap; |
| | | uniforms.envMapIntensity.value = material.envMapIntensity; |
| | | |
| | | // don't flip CubeTexture envMaps, flip everything else: |
| | | // WebGLRenderTargetCube will be flipped for backwards compatibility |
| | | // WebGLRenderTargetCube.texture will be flipped because it's a Texture and NOT a CubeTexture |
| | | // this check must be handled differently, or removed entirely, if WebGLRenderTargetCube uses a CubeTexture in the future |
| | | uniforms.flipEnvMap.value = material.envMap.isCubeTexture ? - 1 : 1; |
| | | |
| | | uniforms.reflectivity.value = material.reflectivity; |
| | | uniforms.refractionRatio.value = material.refractionRatio; |
| | | |
| | | uniforms.maxMipLevel.value = renderer.properties.get(material.envMap).__maxMipLevel; |
| | | |
| | | } |
| | | |
| | | uniforms.specular.value.copy(material.specular); |
| | | uniforms.glossiness.value = material.glossiness; |
| | | |
| | | uniforms.glossinessMap.value = material.glossinessMap; |
| | | |
| | | uniforms.emissiveMap.value = material.emissiveMap; |
| | | uniforms.bumpMap.value = material.bumpMap; |
| | | uniforms.normalMap.value = material.normalMap; |
| | | |
| | | uniforms.displacementMap.value = material.displacementMap; |
| | | uniforms.displacementScale.value = material.displacementScale; |
| | | uniforms.displacementBias.value = material.displacementBias; |
| | | |
| | | if (uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined) { |
| | | |
| | | defines.USE_GLOSSINESSMAP = ''; |
| | | // set USE_ROUGHNESSMAP to enable vUv |
| | | defines.USE_ROUGHNESSMAP = ''; |
| | | |
| | | } |
| | | |
| | | if (uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined) { |
| | | |
| | | delete defines.USE_GLOSSINESSMAP; |
| | | delete defines.USE_ROUGHNESSMAP; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | } |
| | | |
| | | /*********************************/ |
| | | /********** INTERPOLATION ********/ |
| | | /*********************************/ |
| | | |
| | | // Spline Interpolation |
| | | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation |
| | | function GLTFCubicSplineInterpolant(parameterPositions, sampleValues, sampleSize, resultBuffer) { |
| | | |
| | | Interpolant.call(this, parameterPositions, sampleValues, sampleSize, resultBuffer); |
| | | |
| | | } |
| | | |
| | | GLTFCubicSplineInterpolant.prototype = Object.create(Interpolant.prototype); |
| | | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; |
| | | |
| | | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function (index) { |
| | | |
| | | // Copies a sample value to the result buffer. See description of glTF |
| | | // CUBICSPLINE values layout in interpolate_() function below. |
| | | |
| | | var result = this.resultBuffer, |
| | | values = this.sampleValues, |
| | | valueSize = this.valueSize, |
| | | offset = index * valueSize * 3 + valueSize; |
| | | |
| | | for (var i = 0; i !== valueSize; i++) { |
| | | |
| | | result[i] = values[offset + i]; |
| | | |
| | | } |
| | | |
| | | return result; |
| | | |
| | | }; |
| | | |
| | | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; |
| | | |
| | | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; |
| | | |
| | | GLTFCubicSplineInterpolant.prototype.interpolate_ = function (i1, t0, t, t1) { |
| | | |
| | | var result = this.resultBuffer; |
| | | var values = this.sampleValues; |
| | | var stride = this.valueSize; |
| | | |
| | | var stride2 = stride * 2; |
| | | var stride3 = stride * 3; |
| | | |
| | | var td = t1 - t0; |
| | | |
| | | var p = (t - t0) / td; |
| | | var pp = p * p; |
| | | var ppp = pp * p; |
| | | |
| | | var offset1 = i1 * stride3; |
| | | var offset0 = offset1 - stride3; |
| | | |
| | | var s2 = - 2 * ppp + 3 * pp; |
| | | var s3 = ppp - pp; |
| | | var s0 = 1 - s2; |
| | | var s1 = s3 - pp + p; |
| | | |
| | | // Layout of keyframe output values for CUBICSPLINE animations: |
| | | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] |
| | | for (var i = 0; i !== stride; i++) { |
| | | |
| | | var p0 = values[offset0 + i + stride]; // splineVertex_k |
| | | var m0 = values[offset0 + i + stride2] * td; // outTangent_k * (t_k+1 - t_k) |
| | | var p1 = values[offset1 + i + stride]; // splineVertex_k+1 |
| | | var m1 = values[offset1 + i] * td; // inTangent_k+1 * (t_k+1 - t_k) |
| | | |
| | | result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; |
| | | |
| | | } |
| | | |
| | | return result; |
| | | |
| | | }; |
| | | |
| | | /*********************************/ |
| | | /********** INTERNALS ************/ |
| | | /*********************************/ |
| | | |
| | | /* CONSTANTS */ |
| | | |
| | | var WEBGL_CONSTANTS = { |
| | | FLOAT: 5126, |
| | | //FLOAT_MAT2: 35674, |
| | | FLOAT_MAT3: 35675, |
| | | FLOAT_MAT4: 35676, |
| | | FLOAT_VEC2: 35664, |
| | | FLOAT_VEC3: 35665, |
| | | FLOAT_VEC4: 35666, |
| | | LINEAR: 9729, |
| | | REPEAT: 10497, |
| | | SAMPLER_2D: 35678, |
| | | POINTS: 0, |
| | | LINES: 1, |
| | | LINE_LOOP: 2, |
| | | LINE_STRIP: 3, |
| | | TRIANGLES: 4, |
| | | TRIANGLE_STRIP: 5, |
| | | TRIANGLE_FAN: 6, |
| | | UNSIGNED_BYTE: 5121, |
| | | UNSIGNED_SHORT: 5123 |
| | | }; |
| | | |
| | | var WEBGL_COMPONENT_TYPES = { |
| | | 5120: Int8Array, |
| | | 5121: Uint8Array, |
| | | 5122: Int16Array, |
| | | 5123: Uint16Array, |
| | | 5125: Uint32Array, |
| | | 5126: Float32Array |
| | | }; |
| | | |
| | | var WEBGL_FILTERS = { |
| | | 9728: NearestFilter, |
| | | 9729: LinearFilter, |
| | | 9984: NearestMipmapNearestFilter, |
| | | 9985: LinearMipmapNearestFilter, |
| | | 9986: NearestMipmapLinearFilter, |
| | | 9987: LinearMipmapLinearFilter |
| | | }; |
| | | |
| | | var WEBGL_WRAPPINGS = { |
| | | 33071: ClampToEdgeWrapping, |
| | | 33648: MirroredRepeatWrapping, |
| | | 10497: RepeatWrapping |
| | | }; |
| | | |
| | | var WEBGL_TYPE_SIZES = { |
| | | 'SCALAR': 1, |
| | | 'VEC2': 2, |
| | | 'VEC3': 3, |
| | | 'VEC4': 4, |
| | | 'MAT2': 4, |
| | | 'MAT3': 9, |
| | | 'MAT4': 16 |
| | | }; |
| | | |
| | | var ATTRIBUTES = { |
| | | POSITION: 'position', |
| | | NORMAL: 'normal', |
| | | TANGENT: 'tangent', |
| | | TEXCOORD_0: 'uv', |
| | | TEXCOORD_1: 'uv2', |
| | | COLOR_0: 'color', |
| | | WEIGHTS_0: 'skinWeight', |
| | | JOINTS_0: 'skinIndex', |
| | | }; |
| | | |
| | | var PATH_PROPERTIES = { |
| | | scale: 'scale', |
| | | translation: 'position', |
| | | rotation: 'quaternion', |
| | | weights: 'morphTargetInfluences' |
| | | }; |
| | | |
| | | var INTERPOLATION = { |
| | | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each |
| | | // keyframe track will be initialized with a default interpolation type, then modified. |
| | | LINEAR: InterpolateLinear, |
| | | STEP: InterpolateDiscrete |
| | | }; |
| | | |
| | | var ALPHA_MODES = { |
| | | OPAQUE: 'OPAQUE', |
| | | MASK: 'MASK', |
| | | BLEND: 'BLEND' |
| | | }; |
| | | |
| | | var MIME_TYPE_FORMATS = { |
| | | 'image/png': RGBAFormat, |
| | | 'image/jpeg': RGBFormat |
| | | }; |
| | | |
| | | /* UTILITY FUNCTIONS */ |
| | | |
| | | function resolveURL(url, path) { |
| | | |
| | | // Invalid URL |
| | | if (typeof url !== 'string' || url === '') return ''; |
| | | |
| | | // Host Relative URL |
| | | if (/^https?:\/\//i.test(path) && /^\//.test(url)) { |
| | | |
| | | path = path.replace(/(^https?:\/\/[^\/]+).*/i, '$1'); |
| | | |
| | | } |
| | | |
| | | // Absolute URL http://,https://,// |
| | | if (/^(https?:)?\/\//i.test(url)) return url; |
| | | |
| | | // Data URI |
| | | if (/^data:.*,.*$/i.test(url)) return url; |
| | | |
| | | // Blob URL |
| | | if (/^blob:.*$/i.test(url)) return url; |
| | | |
| | | // Relative URL |
| | | return path + url; |
| | | |
| | | } |
| | | |
| | | var defaultMaterial; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material |
| | | */ |
| | | function createDefaultMaterial() { |
| | | |
| | | defaultMaterial = defaultMaterial || new MeshStandardMaterial({ |
| | | color: 0xFFFFFF, |
| | | emissive: 0x000000, |
| | | metalness: 1, |
| | | roughness: 1, |
| | | transparent: false, |
| | | depthTest: true, |
| | | side: FrontSide |
| | | }); |
| | | |
| | | return defaultMaterial; |
| | | |
| | | } |
| | | |
| | | function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) { |
| | | |
| | | // Add unknown glTF extensions to an object's userData. |
| | | |
| | | for (var name in objectDef.extensions) { |
| | | |
| | | if (knownExtensions[name] === undefined) { |
| | | |
| | | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; |
| | | object.userData.gltfExtensions[name] = objectDef.extensions[name]; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * @param {Object3D|Material|BufferGeometry} object |
| | | * @param {GLTF.definition} gltfDef |
| | | */ |
| | | function assignExtrasToUserData(object, gltfDef) { |
| | | |
| | | if (gltfDef.extras !== undefined) { |
| | | |
| | | if (typeof gltfDef.extras === 'object') { |
| | | |
| | | Object.assign(object.userData, gltfDef.extras); |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets |
| | | * |
| | | * @param {BufferGeometry} geometry |
| | | * @param {Array<GLTF.Target>} targets |
| | | * @param {GLTFParser} parser |
| | | * @return {Promise<BufferGeometry>} |
| | | */ |
| | | function addMorphTargets(geometry, targets, parser) { |
| | | |
| | | var hasMorphPosition = false; |
| | | var hasMorphNormal = false; |
| | | |
| | | for (var i = 0, il = targets.length; i < il; i++) { |
| | | |
| | | var target = targets[i]; |
| | | |
| | | if (target.POSITION !== undefined) hasMorphPosition = true; |
| | | if (target.NORMAL !== undefined) hasMorphNormal = true; |
| | | |
| | | if (hasMorphPosition && hasMorphNormal) break; |
| | | |
| | | } |
| | | |
| | | if (!hasMorphPosition && !hasMorphNormal) return Promise.resolve(geometry); |
| | | |
| | | var pendingPositionAccessors = []; |
| | | var pendingNormalAccessors = []; |
| | | |
| | | for (var i = 0, il = targets.length; i < il; i++) { |
| | | |
| | | var target = targets[i]; |
| | | |
| | | if (hasMorphPosition) { |
| | | |
| | | var pendingAccessor = target.POSITION !== undefined |
| | | ? parser.getDependency('accessor', target.POSITION) |
| | | : geometry.attributes.position; |
| | | |
| | | pendingPositionAccessors.push(pendingAccessor); |
| | | |
| | | } |
| | | |
| | | if (hasMorphNormal) { |
| | | |
| | | var pendingAccessor = target.NORMAL !== undefined |
| | | ? parser.getDependency('accessor', target.NORMAL) |
| | | : geometry.attributes.normal; |
| | | |
| | | pendingNormalAccessors.push(pendingAccessor); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return Promise.all([ |
| | | Promise.all(pendingPositionAccessors), |
| | | Promise.all(pendingNormalAccessors) |
| | | ]).then(function (accessors) { |
| | | |
| | | var morphPositions = accessors[0]; |
| | | var morphNormals = accessors[1]; |
| | | |
| | | // Clone morph target accessors before modifying them. |
| | | |
| | | for (var i = 0, il = morphPositions.length; i < il; i++) { |
| | | |
| | | if (geometry.attributes.position === morphPositions[i]) continue; |
| | | |
| | | morphPositions[i] = cloneBufferAttribute(morphPositions[i]); |
| | | |
| | | } |
| | | |
| | | for (var i = 0, il = morphNormals.length; i < il; i++) { |
| | | |
| | | if (geometry.attributes.normal === morphNormals[i]) continue; |
| | | |
| | | morphNormals[i] = cloneBufferAttribute(morphNormals[i]); |
| | | |
| | | } |
| | | |
| | | for (var i = 0, il = targets.length; i < il; i++) { |
| | | |
| | | var target = targets[i]; |
| | | var attributeName = 'morphTarget' + i; |
| | | |
| | | if (hasMorphPosition) { |
| | | |
| | | // Three.js morph position is absolute value. The formula is |
| | | // basePosition |
| | | // + weight0 * ( morphPosition0 - basePosition ) |
| | | // + weight1 * ( morphPosition1 - basePosition ) |
| | | // ... |
| | | // while the glTF one is relative |
| | | // basePosition |
| | | // + weight0 * glTFmorphPosition0 |
| | | // + weight1 * glTFmorphPosition1 |
| | | // ... |
| | | // then we need to convert from relative to absolute here. |
| | | |
| | | if (target.POSITION !== undefined) { |
| | | |
| | | var positionAttribute = morphPositions[i]; |
| | | positionAttribute.name = attributeName; |
| | | |
| | | var position = geometry.attributes.position; |
| | | |
| | | for (var j = 0, jl = positionAttribute.count; j < jl; j++) { |
| | | |
| | | positionAttribute.setXYZ( |
| | | j, |
| | | positionAttribute.getX(j) + position.getX(j), |
| | | positionAttribute.getY(j) + position.getY(j), |
| | | positionAttribute.getZ(j) + position.getZ(j) |
| | | ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (hasMorphNormal) { |
| | | |
| | | // see target.POSITION's comment |
| | | |
| | | if (target.NORMAL !== undefined) { |
| | | |
| | | var normalAttribute = morphNormals[i]; |
| | | normalAttribute.name = attributeName; |
| | | |
| | | var normal = geometry.attributes.normal; |
| | | |
| | | for (var j = 0, jl = normalAttribute.count; j < jl; j++) { |
| | | |
| | | normalAttribute.setXYZ( |
| | | j, |
| | | normalAttribute.getX(j) + normal.getX(j), |
| | | normalAttribute.getY(j) + normal.getY(j), |
| | | normalAttribute.getZ(j) + normal.getZ(j) |
| | | ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (hasMorphPosition) geometry.morphAttributes.position = morphPositions; |
| | | if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals; |
| | | |
| | | return geometry; |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * @param {Mesh} mesh |
| | | * @param {GLTF.Mesh} meshDef |
| | | */ |
| | | function updateMorphTargets(mesh, meshDef) { |
| | | |
| | | mesh.updateMorphTargets(); |
| | | |
| | | if (meshDef.weights !== undefined) { |
| | | |
| | | for (var i = 0, il = meshDef.weights.length; i < il; i++) { |
| | | |
| | | mesh.morphTargetInfluences[i] = meshDef.weights[i]; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // .extras has user-defined data, so check that .extras.targetNames is an array. |
| | | if (meshDef.extras && Array.isArray(meshDef.extras.targetNames)) { |
| | | |
| | | var targetNames = meshDef.extras.targetNames; |
| | | |
| | | if (mesh.morphTargetInfluences.length === targetNames.length) { |
| | | |
| | | mesh.morphTargetDictionary = {}; |
| | | |
| | | for (var i = 0, il = targetNames.length; i < il; i++) { |
| | | |
| | | mesh.morphTargetDictionary[targetNames[i]] = i; |
| | | |
| | | } |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | function createPrimitiveKey(primitiveDef) { |
| | | |
| | | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]; |
| | | var geometryKey; |
| | | |
| | | if (dracoExtension) { |
| | | |
| | | geometryKey = 'draco:' + dracoExtension.bufferView |
| | | + ':' + dracoExtension.indices |
| | | + ':' + createAttributesKey(dracoExtension.attributes); |
| | | |
| | | } else { |
| | | |
| | | geometryKey = primitiveDef.indices + ':' + createAttributesKey(primitiveDef.attributes) + ':' + primitiveDef.mode; |
| | | |
| | | } |
| | | |
| | | return geometryKey; |
| | | |
| | | } |
| | | |
| | | function createAttributesKey(attributes) { |
| | | |
| | | var attributesKey = ''; |
| | | |
| | | var keys = Object.keys(attributes).sort(); |
| | | |
| | | for (var i = 0, il = keys.length; i < il; i++) { |
| | | |
| | | attributesKey += keys[i] + ':' + attributes[keys[i]] + ';'; |
| | | |
| | | } |
| | | |
| | | return attributesKey; |
| | | |
| | | } |
| | | |
| | | function cloneBufferAttribute(attribute) { |
| | | |
| | | if (attribute.isInterleavedBufferAttribute) { |
| | | |
| | | var count = attribute.count; |
| | | var itemSize = attribute.itemSize; |
| | | var array = attribute.array.slice(0, count * itemSize); |
| | | |
| | | for (var i = 0, j = 0; i < count; ++i) { |
| | | |
| | | array[j++] = attribute.getX(i); |
| | | if (itemSize >= 2) array[j++] = attribute.getY(i); |
| | | if (itemSize >= 3) array[j++] = attribute.getZ(i); |
| | | if (itemSize >= 4) array[j++] = attribute.getW(i); |
| | | |
| | | } |
| | | |
| | | return new BufferAttribute(array, itemSize, attribute.normalized); |
| | | |
| | | } |
| | | |
| | | return attribute.clone(); |
| | | |
| | | } |
| | | |
| | | /* GLTF PARSER */ |
| | | |
| | | function GLTFParser(json, extensions, options) { |
| | | |
| | | this.json = json || {}; |
| | | this.extensions = extensions || {}; |
| | | this.options = options || {}; |
| | | |
| | | // loader object cache |
| | | this.cache = new GLTFRegistry(); |
| | | |
| | | // BufferGeometry caching |
| | | this.primitiveCache = {}; |
| | | |
| | | this.textureLoader = new TextureLoader(this.options.manager); |
| | | this.textureLoader.setCrossOrigin(this.options.crossOrigin); |
| | | |
| | | this.fileLoader = new FileLoader(this.options.manager); |
| | | this.fileLoader.setResponseType('arraybuffer'); |
| | | |
| | | if (this.options.crossOrigin === 'use-credentials') { |
| | | |
| | | this.fileLoader.setWithCredentials(true); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | GLTFParser.prototype.parse = function (onLoad, onError) { |
| | | |
| | | var parser = this; |
| | | var json = this.json; |
| | | var extensions = this.extensions; |
| | | |
| | | // Clear the loader cache |
| | | this.cache.removeAll(); |
| | | |
| | | // Mark the special nodes/meshes in json for efficient parse |
| | | this.markDefs(); |
| | | |
| | | Promise.all([ |
| | | |
| | | this.getDependencies('scene'), |
| | | this.getDependencies('animation'), |
| | | this.getDependencies('camera'), |
| | | |
| | | ]).then(function (dependencies) { |
| | | |
| | | var result = { |
| | | scene: dependencies[0][json.scene || 0], |
| | | scenes: dependencies[0], |
| | | animations: dependencies[1], |
| | | cameras: dependencies[2], |
| | | asset: json.asset, |
| | | parser: parser, |
| | | userData: {} |
| | | }; |
| | | |
| | | addUnknownExtensionsToUserData(extensions, result, json); |
| | | |
| | | assignExtrasToUserData(result, json); |
| | | |
| | | onLoad(result); |
| | | |
| | | }).catch(onError); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Marks the special nodes/meshes in json for efficient parse. |
| | | */ |
| | | GLTFParser.prototype.markDefs = function () { |
| | | |
| | | var nodeDefs = this.json.nodes || []; |
| | | var skinDefs = this.json.skins || []; |
| | | var meshDefs = this.json.meshes || []; |
| | | |
| | | var meshReferences = {}; |
| | | var meshUses = {}; |
| | | |
| | | // Nothing in the node definition indicates whether it is a Bone or an |
| | | // Object3D. Use the skins' joint references to mark bones. |
| | | for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) { |
| | | |
| | | var joints = skinDefs[skinIndex].joints; |
| | | |
| | | for (var i = 0, il = joints.length; i < il; i++) { |
| | | |
| | | nodeDefs[joints[i]].isBone = true; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // Meshes can (and should) be reused by multiple nodes in a glTF asset. To |
| | | // avoid having more than one Mesh with the same name, count |
| | | // references and rename instances below. |
| | | // |
| | | // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. |
| | | for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { |
| | | |
| | | var nodeDef = nodeDefs[nodeIndex]; |
| | | |
| | | if (nodeDef.mesh !== undefined) { |
| | | |
| | | if (meshReferences[nodeDef.mesh] === undefined) { |
| | | |
| | | meshReferences[nodeDef.mesh] = meshUses[nodeDef.mesh] = 0; |
| | | |
| | | } |
| | | |
| | | meshReferences[nodeDef.mesh]++; |
| | | |
| | | // Nothing in the mesh definition indicates whether it is |
| | | // a SkinnedMesh or Mesh. Use the node's mesh reference |
| | | // to mark SkinnedMesh if node has skin. |
| | | if (nodeDef.skin !== undefined) { |
| | | |
| | | meshDefs[nodeDef.mesh].isSkinnedMesh = true; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | this.json.meshReferences = meshReferences; |
| | | this.json.meshUses = meshUses; |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Requests the specified dependency asynchronously, with caching. |
| | | * @param {string} type |
| | | * @param {number} index |
| | | * @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>} |
| | | */ |
| | | GLTFParser.prototype.getDependency = function (type, index) { |
| | | |
| | | var cacheKey = type + ':' + index; |
| | | var dependency = this.cache.get(cacheKey); |
| | | |
| | | if (!dependency) { |
| | | |
| | | switch (type) { |
| | | |
| | | case 'scene': |
| | | dependency = this.loadScene(index); |
| | | break; |
| | | |
| | | case 'node': |
| | | dependency = this.loadNode(index); |
| | | break; |
| | | |
| | | case 'mesh': |
| | | dependency = this.loadMesh(index); |
| | | break; |
| | | |
| | | case 'accessor': |
| | | dependency = this.loadAccessor(index); |
| | | break; |
| | | |
| | | case 'bufferView': |
| | | dependency = this.loadBufferView(index); |
| | | break; |
| | | |
| | | case 'buffer': |
| | | dependency = this.loadBuffer(index); |
| | | break; |
| | | |
| | | case 'material': |
| | | dependency = this.loadMaterial(index); |
| | | break; |
| | | |
| | | case 'texture': |
| | | dependency = this.loadTexture(index); |
| | | break; |
| | | |
| | | case 'skin': |
| | | dependency = this.loadSkin(index); |
| | | break; |
| | | |
| | | case 'animation': |
| | | dependency = this.loadAnimation(index); |
| | | break; |
| | | |
| | | case 'camera': |
| | | dependency = this.loadCamera(index); |
| | | break; |
| | | |
| | | case 'light': |
| | | dependency = this.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].loadLight(index); |
| | | break; |
| | | |
| | | default: |
| | | throw new Error('Unknown type: ' + type); |
| | | |
| | | } |
| | | |
| | | this.cache.add(cacheKey, dependency); |
| | | |
| | | } |
| | | |
| | | return dependency; |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Requests all dependencies of the specified type asynchronously, with caching. |
| | | * @param {string} type |
| | | * @return {Promise<Array<Object>>} |
| | | */ |
| | | GLTFParser.prototype.getDependencies = function (type) { |
| | | |
| | | var dependencies = this.cache.get(type); |
| | | |
| | | if (!dependencies) { |
| | | |
| | | var parser = this; |
| | | var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || []; |
| | | |
| | | dependencies = Promise.all(defs.map(function (def, index) { |
| | | |
| | | return parser.getDependency(type, index); |
| | | |
| | | })); |
| | | |
| | | this.cache.add(type, dependencies); |
| | | |
| | | } |
| | | |
| | | return dependencies; |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views |
| | | * @param {number} bufferIndex |
| | | * @return {Promise<ArrayBuffer>} |
| | | */ |
| | | GLTFParser.prototype.loadBuffer = function (bufferIndex) { |
| | | |
| | | var bufferDef = this.json.buffers[bufferIndex]; |
| | | var loader = this.fileLoader; |
| | | |
| | | if (bufferDef.type && bufferDef.type !== 'arraybuffer') { |
| | | |
| | | throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.'); |
| | | |
| | | } |
| | | |
| | | // If present, GLB container is required to be the first buffer. |
| | | if (bufferDef.uri === undefined && bufferIndex === 0) { |
| | | |
| | | return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body); |
| | | |
| | | } |
| | | |
| | | var options = this.options; |
| | | |
| | | return new Promise(function (resolve, reject) { |
| | | |
| | | loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () { |
| | | |
| | | reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".')); |
| | | |
| | | }); |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views |
| | | * @param {number} bufferViewIndex |
| | | * @return {Promise<ArrayBuffer>} |
| | | */ |
| | | GLTFParser.prototype.loadBufferView = function (bufferViewIndex) { |
| | | |
| | | var bufferViewDef = this.json.bufferViews[bufferViewIndex]; |
| | | |
| | | return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) { |
| | | |
| | | var byteLength = bufferViewDef.byteLength || 0; |
| | | var byteOffset = bufferViewDef.byteOffset || 0; |
| | | return buffer.slice(byteOffset, byteOffset + byteLength); |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors |
| | | * @param {number} accessorIndex |
| | | * @return {Promise<BufferAttribute|InterleavedBufferAttribute>} |
| | | */ |
| | | GLTFParser.prototype.loadAccessor = function (accessorIndex) { |
| | | |
| | | var parser = this; |
| | | var json = this.json; |
| | | |
| | | var accessorDef = this.json.accessors[accessorIndex]; |
| | | |
| | | if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) { |
| | | |
| | | // Ignore empty accessors, which may be used to declare runtime |
| | | // information about attributes coming from another source (e.g. Draco |
| | | // compression extension). |
| | | return Promise.resolve(null); |
| | | |
| | | } |
| | | |
| | | var pendingBufferViews = []; |
| | | |
| | | if (accessorDef.bufferView !== undefined) { |
| | | |
| | | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView)); |
| | | |
| | | } else { |
| | | |
| | | pendingBufferViews.push(null); |
| | | |
| | | } |
| | | |
| | | if (accessorDef.sparse !== undefined) { |
| | | |
| | | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView)); |
| | | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView)); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pendingBufferViews).then(function (bufferViews) { |
| | | |
| | | var bufferView = bufferViews[0]; |
| | | |
| | | var itemSize = WEBGL_TYPE_SIZES[accessorDef.type]; |
| | | var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; |
| | | |
| | | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. |
| | | var elementBytes = TypedArray.BYTES_PER_ELEMENT; |
| | | var itemBytes = elementBytes * itemSize; |
| | | var byteOffset = accessorDef.byteOffset || 0; |
| | | var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined; |
| | | var normalized = accessorDef.normalized === true; |
| | | var array, bufferAttribute; |
| | | |
| | | // The buffer is not interleaved if the stride is the item size in bytes. |
| | | if (byteStride && byteStride !== itemBytes) { |
| | | |
| | | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer |
| | | // This makes sure that IBA.count reflects accessor.count properly |
| | | var ibSlice = Math.floor(byteOffset / byteStride); |
| | | var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; |
| | | var ib = parser.cache.get(ibCacheKey); |
| | | |
| | | if (!ib) { |
| | | |
| | | array = new TypedArray(bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes); |
| | | |
| | | // Integer parameters to IB/IBA are in array elements, not bytes. |
| | | ib = new InterleavedBuffer(array, byteStride / elementBytes); |
| | | |
| | | parser.cache.add(ibCacheKey, ib); |
| | | |
| | | } |
| | | |
| | | bufferAttribute = new InterleavedBufferAttribute(ib, itemSize, (byteOffset % byteStride) / elementBytes, normalized); |
| | | |
| | | } else { |
| | | |
| | | if (bufferView === null) { |
| | | |
| | | array = new TypedArray(accessorDef.count * itemSize); |
| | | |
| | | } else { |
| | | |
| | | array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize); |
| | | |
| | | } |
| | | |
| | | bufferAttribute = new BufferAttribute(array, itemSize, normalized); |
| | | |
| | | } |
| | | |
| | | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors |
| | | if (accessorDef.sparse !== undefined) { |
| | | |
| | | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; |
| | | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType]; |
| | | |
| | | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; |
| | | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; |
| | | |
| | | var sparseIndices = new TypedArrayIndices(bufferViews[1], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices); |
| | | var sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize); |
| | | |
| | | if (bufferView !== null) { |
| | | |
| | | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. |
| | | bufferAttribute.setArray(bufferAttribute.array.slice()); |
| | | |
| | | } |
| | | |
| | | for (var i = 0, il = sparseIndices.length; i < il; i++) { |
| | | |
| | | var index = sparseIndices[i]; |
| | | |
| | | bufferAttribute.setX(index, sparseValues[i * itemSize]); |
| | | if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]); |
| | | if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]); |
| | | if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]); |
| | | if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return bufferAttribute; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures |
| | | * @param {number} textureIndex |
| | | * @return {Promise<THREE.Texture>} |
| | | */ |
| | | GLTFParser.prototype.loadTexture = function (textureIndex) { |
| | | |
| | | var parser = this; |
| | | var json = this.json; |
| | | var options = this.options; |
| | | var textureLoader = this.textureLoader; |
| | | |
| | | var URL = window.URL || window.webkitURL; |
| | | |
| | | var textureDef = json.textures[textureIndex]; |
| | | |
| | | var textureExtensions = textureDef.extensions || {}; |
| | | |
| | | var source; |
| | | |
| | | if (textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS]) { |
| | | |
| | | source = json.images[textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS].source]; |
| | | |
| | | } else { |
| | | |
| | | source = json.images[textureDef.source]; |
| | | |
| | | } |
| | | |
| | | var sourceURI = source.uri; |
| | | var isObjectURL = false; |
| | | |
| | | if (source.bufferView !== undefined) { |
| | | |
| | | // Load binary image data from bufferView, if provided. |
| | | |
| | | sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) { |
| | | if (window.arrayBufferToBase64 != undefined) { |
| | | var base64 = window.arrayBufferToBase64(bufferView) |
| | | var base64String = `data:${source.mimeType};base64,${base64}` |
| | | return base64String |
| | | } |
| | | isObjectURL = true; |
| | | var blob = new Blob([bufferView], { type: source.mimeType }); |
| | | sourceURI = URL.createObjectURL(blob); |
| | | return sourceURI; |
| | | }); |
| | | |
| | | } |
| | | |
| | | return Promise.resolve(sourceURI).then(function (sourceURI) { |
| | | |
| | | // Load Texture resource. |
| | | |
| | | var loader = Loader.Handlers.get(sourceURI); |
| | | |
| | | if (!loader) { |
| | | |
| | | loader = textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS] |
| | | ? parser.extensions[EXTENSIONS.MSFT_TEXTURE_DDS].ddsLoader |
| | | : textureLoader; |
| | | |
| | | } |
| | | |
| | | return new Promise(function (resolve, reject) { |
| | | |
| | | loader.load(resolveURL(sourceURI, options.path), resolve, undefined, reject); |
| | | |
| | | }); |
| | | |
| | | }).then(function (texture) { |
| | | |
| | | // Clean up resources and configure Texture. |
| | | if (window.arrayBufferToBase64 == undefined) { |
| | | if (isObjectURL === true) { |
| | | |
| | | URL.revokeObjectURL(sourceURI); |
| | | |
| | | } |
| | | } |
| | | |
| | | texture.flipY = false; |
| | | |
| | | if (textureDef.name !== undefined) texture.name = textureDef.name; |
| | | |
| | | // Ignore unknown mime types, like DDS files. |
| | | if (source.mimeType in MIME_TYPE_FORMATS) { |
| | | |
| | | texture.format = MIME_TYPE_FORMATS[source.mimeType]; |
| | | |
| | | } |
| | | |
| | | var samplers = json.samplers || {}; |
| | | var sampler = samplers[textureDef.sampler] || {}; |
| | | |
| | | texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || LinearFilter; |
| | | texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || LinearMipmapLinearFilter; |
| | | texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || RepeatWrapping; |
| | | texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || RepeatWrapping; |
| | | |
| | | return texture; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Asynchronously assigns a texture to the given material parameters. |
| | | * @param {Object} materialParams |
| | | * @param {string} mapName |
| | | * @param {Object} mapDef |
| | | * @return {Promise} |
| | | */ |
| | | GLTFParser.prototype.assignTexture = function (materialParams, mapName, mapDef) { |
| | | |
| | | var parser = this; |
| | | |
| | | return this.getDependency('texture', mapDef.index).then(function (texture) { |
| | | |
| | | if (!texture.isCompressedTexture) { |
| | | |
| | | switch (mapName) { |
| | | |
| | | case 'aoMap': |
| | | case 'emissiveMap': |
| | | case 'metalnessMap': |
| | | case 'normalMap': |
| | | case 'roughnessMap': |
| | | texture.format = RGBFormat; |
| | | break; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM]) { |
| | | |
| | | var transform = mapDef.extensions !== undefined ? mapDef.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] : undefined; |
| | | |
| | | if (transform) { |
| | | |
| | | texture = parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM].extendTexture(texture, transform); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | materialParams[mapName] = texture; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Assigns final material to a Mesh, Line, or Points instance. The instance |
| | | * already has a material (generated from the glTF material options alone) |
| | | * but reuse of the same glTF material may require multiple threejs materials |
| | | * to accomodate different primitive types, defines, etc. New materials will |
| | | * be created if necessary, and reused from a cache. |
| | | * @param {Object3D} mesh Mesh, Line, or Points instance. |
| | | */ |
| | | GLTFParser.prototype.assignFinalMaterial = function (mesh) { |
| | | |
| | | var geometry = mesh.geometry; |
| | | var material = mesh.material; |
| | | var extensions = this.extensions; |
| | | |
| | | var useVertexTangents = geometry.attributes.tangent !== undefined; |
| | | var useVertexColors = geometry.attributes.color !== undefined; |
| | | var useFlatShading = geometry.attributes.normal === undefined; |
| | | var useSkinning = mesh.isSkinnedMesh === true; |
| | | var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0; |
| | | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; |
| | | |
| | | if (mesh.isPoints) { |
| | | |
| | | var cacheKey = 'PointsMaterial:' + material.uuid; |
| | | |
| | | var pointsMaterial = this.cache.get(cacheKey); |
| | | |
| | | if (!pointsMaterial) { |
| | | |
| | | pointsMaterial = new PointsMaterial(); |
| | | Material.prototype.copy.call(pointsMaterial, material); |
| | | pointsMaterial.color.copy(material.color); |
| | | pointsMaterial.map = material.map; |
| | | pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet |
| | | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px |
| | | |
| | | this.cache.add(cacheKey, pointsMaterial); |
| | | |
| | | } |
| | | |
| | | material = pointsMaterial; |
| | | |
| | | } else if (mesh.isLine) { |
| | | |
| | | var cacheKey = 'LineBasicMaterial:' + material.uuid; |
| | | |
| | | var lineMaterial = this.cache.get(cacheKey); |
| | | |
| | | if (!lineMaterial) { |
| | | |
| | | lineMaterial = new LineBasicMaterial(); |
| | | Material.prototype.copy.call(lineMaterial, material); |
| | | lineMaterial.color.copy(material.color); |
| | | lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet |
| | | |
| | | this.cache.add(cacheKey, lineMaterial); |
| | | |
| | | } |
| | | |
| | | material = lineMaterial; |
| | | |
| | | } |
| | | |
| | | // Clone the material if it will be modified |
| | | if (useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets) { |
| | | |
| | | var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; |
| | | |
| | | if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:'; |
| | | if (useSkinning) cacheKey += 'skinning:'; |
| | | if (useVertexTangents) cacheKey += 'vertex-tangents:'; |
| | | if (useVertexColors) cacheKey += 'vertex-colors:'; |
| | | if (useFlatShading) cacheKey += 'flat-shading:'; |
| | | if (useMorphTargets) cacheKey += 'morph-targets:'; |
| | | if (useMorphNormals) cacheKey += 'morph-normals:'; |
| | | |
| | | var cachedMaterial = this.cache.get(cacheKey); |
| | | |
| | | if (!cachedMaterial) { |
| | | |
| | | cachedMaterial = material.isGLTFSpecularGlossinessMaterial |
| | | ? extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].cloneMaterial(material) |
| | | : material.clone(); |
| | | |
| | | if (useSkinning) cachedMaterial.skinning = true; |
| | | if (useVertexTangents) cachedMaterial.vertexTangents = true; |
| | | if (useVertexColors) cachedMaterial.vertexColors = VertexColors; |
| | | if (useFlatShading) cachedMaterial.flatShading = true; |
| | | if (useMorphTargets) cachedMaterial.morphTargets = true; |
| | | if (useMorphNormals) cachedMaterial.morphNormals = true; |
| | | |
| | | this.cache.add(cacheKey, cachedMaterial); |
| | | |
| | | } |
| | | |
| | | material = cachedMaterial; |
| | | |
| | | } |
| | | |
| | | // workarounds for mesh and geometry |
| | | |
| | | if (material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined) { |
| | | |
| | | console.log('THREE.GLTFLoader: Duplicating UVs to support aoMap.'); |
| | | geometry.addAttribute('uv2', new BufferAttribute(geometry.attributes.uv.array, 2)); |
| | | |
| | | } |
| | | |
| | | if (material.isGLTFSpecularGlossinessMaterial) { |
| | | |
| | | // for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update |
| | | mesh.onBeforeRender = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].refreshUniforms; |
| | | |
| | | } |
| | | |
| | | mesh.material = material; |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials |
| | | * @param {number} materialIndex |
| | | * @return {Promise<Material>} |
| | | */ |
| | | GLTFParser.prototype.loadMaterial = function (materialIndex) { |
| | | |
| | | var parser = this; |
| | | var json = this.json; |
| | | var extensions = this.extensions; |
| | | var materialDef = json.materials[materialIndex]; |
| | | |
| | | var materialType; |
| | | var materialParams = {}; |
| | | var materialExtensions = materialDef.extensions || {}; |
| | | |
| | | var pending = []; |
| | | |
| | | if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) { |
| | | |
| | | var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]; |
| | | materialType = sgExtension.getMaterialType(); |
| | | pending.push(sgExtension.extendParams(materialParams, materialDef, parser)); |
| | | |
| | | } else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) { |
| | | |
| | | var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT]; |
| | | materialType = kmuExtension.getMaterialType(); |
| | | pending.push(kmuExtension.extendParams(materialParams, materialDef, parser)); |
| | | |
| | | } else { |
| | | |
| | | // Specification: |
| | | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material |
| | | |
| | | materialType = MeshStandardMaterial; |
| | | |
| | | var metallicRoughness = materialDef.pbrMetallicRoughness || {}; |
| | | |
| | | materialParams.color = new Color(1.0, 1.0, 1.0); |
| | | materialParams.opacity = 1.0; |
| | | |
| | | if (Array.isArray(metallicRoughness.baseColorFactor)) { |
| | | |
| | | var array = metallicRoughness.baseColorFactor; |
| | | |
| | | materialParams.color.fromArray(array); |
| | | materialParams.opacity = array[3]; |
| | | |
| | | } |
| | | |
| | | if (metallicRoughness.baseColorTexture !== undefined) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture)); |
| | | |
| | | } |
| | | |
| | | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; |
| | | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; |
| | | |
| | | if (metallicRoughness.metallicRoughnessTexture !== undefined) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture)); |
| | | pending.push(parser.assignTexture(materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (materialDef.doubleSided === true) { |
| | | |
| | | materialParams.side = DoubleSide; |
| | | |
| | | } |
| | | |
| | | var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; |
| | | |
| | | if (alphaMode === ALPHA_MODES.BLEND) { |
| | | |
| | | materialParams.transparent = true; |
| | | |
| | | } else { |
| | | |
| | | materialParams.transparent = false; |
| | | |
| | | if (alphaMode === ALPHA_MODES.MASK) { |
| | | |
| | | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'normalMap', materialDef.normalTexture)); |
| | | |
| | | materialParams.normalScale = new Vector2(1, 1); |
| | | |
| | | if (materialDef.normalTexture.scale !== undefined) { |
| | | |
| | | materialParams.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'aoMap', materialDef.occlusionTexture)); |
| | | |
| | | if (materialDef.occlusionTexture.strength !== undefined) { |
| | | |
| | | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial) { |
| | | |
| | | materialParams.emissive = new Color().fromArray(materialDef.emissiveFactor); |
| | | |
| | | } |
| | | |
| | | if (materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial) { |
| | | |
| | | pending.push(parser.assignTexture(materialParams, 'emissiveMap', materialDef.emissiveTexture)); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending).then(function () { |
| | | |
| | | var material; |
| | | |
| | | if (materialType === ShaderMaterial) { |
| | | |
| | | material = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].createMaterial(materialParams); |
| | | |
| | | } else { |
| | | |
| | | material = new materialType(materialParams); |
| | | |
| | | } |
| | | |
| | | if (materialDef.name !== undefined) material.name = materialDef.name; |
| | | |
| | | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. |
| | | if (material.map) material.map.encoding = sRGBEncoding; |
| | | if (material.emissiveMap) material.emissiveMap.encoding = sRGBEncoding; |
| | | if (material.specularMap) material.specularMap.encoding = sRGBEncoding; |
| | | |
| | | assignExtrasToUserData(material, materialDef); |
| | | |
| | | if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef); |
| | | |
| | | return material; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * @param {BufferGeometry} geometry |
| | | * @param {GLTF.Primitive} primitiveDef |
| | | * @param {GLTFParser} parser |
| | | * @return {Promise<BufferGeometry>} |
| | | */ |
| | | function addPrimitiveAttributes(geometry, primitiveDef, parser) { |
| | | |
| | | var attributes = primitiveDef.attributes; |
| | | |
| | | var pending = []; |
| | | |
| | | function assignAttributeAccessor(accessorIndex, attributeName) { |
| | | |
| | | return parser.getDependency('accessor', accessorIndex) |
| | | .then(function (accessor) { |
| | | |
| | | geometry.addAttribute(attributeName, accessor); |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | for (var gltfAttributeName in attributes) { |
| | | |
| | | var threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase(); |
| | | |
| | | // Skip attributes already provided by e.g. Draco extension. |
| | | if (threeAttributeName in geometry.attributes) continue; |
| | | |
| | | pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName)); |
| | | |
| | | } |
| | | |
| | | if (primitiveDef.indices !== undefined && !geometry.index) { |
| | | |
| | | var accessor = parser.getDependency('accessor', primitiveDef.indices).then(function (accessor) { |
| | | |
| | | geometry.setIndex(accessor); |
| | | |
| | | }); |
| | | |
| | | pending.push(accessor); |
| | | |
| | | } |
| | | |
| | | assignExtrasToUserData(geometry, primitiveDef); |
| | | |
| | | return Promise.all(pending).then(function () { |
| | | |
| | | return primitiveDef.targets !== undefined |
| | | ? addMorphTargets(geometry, primitiveDef.targets, parser) |
| | | : geometry; |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry |
| | | * |
| | | * Creates BufferGeometries from primitives. |
| | | * |
| | | * @param {Array<GLTF.Primitive>} primitives |
| | | * @return {Promise<Array<BufferGeometry>>} |
| | | */ |
| | | GLTFParser.prototype.loadGeometries = function (primitives) { |
| | | |
| | | var parser = this; |
| | | var extensions = this.extensions; |
| | | var cache = this.primitiveCache; |
| | | |
| | | function createDracoPrimitive(primitive) { |
| | | |
| | | return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] |
| | | .decodePrimitive(primitive, parser) |
| | | .then(function (geometry) { |
| | | |
| | | return addPrimitiveAttributes(geometry, primitive, parser); |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | var pending = []; |
| | | |
| | | for (var i = 0, il = primitives.length; i < il; i++) { |
| | | |
| | | var primitive = primitives[i]; |
| | | var cacheKey = createPrimitiveKey(primitive); |
| | | |
| | | // See if we've already created this geometry |
| | | var cached = cache[cacheKey]; |
| | | |
| | | if (cached) { |
| | | |
| | | // Use the cached geometry if it exists |
| | | pending.push(cached.promise); |
| | | |
| | | } else { |
| | | |
| | | var geometryPromise; |
| | | |
| | | if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) { |
| | | |
| | | // Use DRACO geometry if available |
| | | geometryPromise = createDracoPrimitive(primitive); |
| | | |
| | | } else { |
| | | |
| | | // Otherwise create a new geometry |
| | | geometryPromise = addPrimitiveAttributes(new BufferGeometry(), primitive, parser); |
| | | |
| | | } |
| | | |
| | | // Cache this geometry |
| | | cache[cacheKey] = { primitive: primitive, promise: geometryPromise }; |
| | | |
| | | pending.push(geometryPromise); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes |
| | | * @param {number} meshIndex |
| | | * @return {Promise<Group|Mesh|SkinnedMesh>} |
| | | */ |
| | | GLTFParser.prototype.loadMesh = function (meshIndex) { |
| | | |
| | | var parser = this; |
| | | var json = this.json; |
| | | |
| | | var meshDef = json.meshes[meshIndex]; |
| | | var primitives = meshDef.primitives; |
| | | |
| | | var pending = []; |
| | | |
| | | for (var i = 0, il = primitives.length; i < il; i++) { |
| | | |
| | | var material = primitives[i].material === undefined |
| | | ? createDefaultMaterial() |
| | | : this.getDependency('material', primitives[i].material); |
| | | |
| | | pending.push(material); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending).then(function (originalMaterials) { |
| | | |
| | | return parser.loadGeometries(primitives).then(function (geometries) { |
| | | |
| | | var meshes = []; |
| | | |
| | | for (var i = 0, il = geometries.length; i < il; i++) { |
| | | |
| | | var geometry = geometries[i]; |
| | | var primitive = primitives[i]; |
| | | |
| | | // 1. create Mesh |
| | | |
| | | var mesh; |
| | | |
| | | var material = originalMaterials[i]; |
| | | |
| | | if (primitive.mode === WEBGL_CONSTANTS.TRIANGLES || |
| | | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || |
| | | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || |
| | | primitive.mode === undefined) { |
| | | |
| | | // .isSkinnedMesh isn't in glTF spec. See .markDefs() |
| | | mesh = meshDef.isSkinnedMesh === true |
| | | ? new SkinnedMesh(geometry, material) |
| | | : new Mesh(geometry, material); |
| | | |
| | | if (mesh.isSkinnedMesh === true && !mesh.geometry.attributes.skinWeight.normalized) { |
| | | |
| | | // we normalize floating point skin weight array to fix malformed assets (see #15319) |
| | | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs |
| | | mesh.normalizeSkinWeights(); |
| | | |
| | | } |
| | | |
| | | if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) { |
| | | |
| | | mesh.drawMode = TriangleStripDrawMode; |
| | | |
| | | } else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) { |
| | | |
| | | mesh.drawMode = TriangleFanDrawMode; |
| | | |
| | | } |
| | | |
| | | } else if (primitive.mode === WEBGL_CONSTANTS.LINES) { |
| | | |
| | | mesh = new LineSegments(geometry, material); |
| | | |
| | | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) { |
| | | |
| | | mesh = new Line(geometry, material); |
| | | |
| | | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) { |
| | | |
| | | mesh = new LineLoop(geometry, material); |
| | | |
| | | } else if (primitive.mode === WEBGL_CONSTANTS.POINTS) { |
| | | |
| | | mesh = new Points(geometry, material); |
| | | |
| | | } else { |
| | | |
| | | throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode); |
| | | |
| | | } |
| | | |
| | | if (Object.keys(mesh.geometry.morphAttributes).length > 0) { |
| | | |
| | | updateMorphTargets(mesh, meshDef); |
| | | |
| | | } |
| | | |
| | | mesh.name = meshDef.name || ('mesh_' + meshIndex); |
| | | |
| | | if (geometries.length > 1) mesh.name += '_' + i; |
| | | |
| | | assignExtrasToUserData(mesh, meshDef); |
| | | |
| | | parser.assignFinalMaterial(mesh); |
| | | |
| | | meshes.push(mesh); |
| | | |
| | | } |
| | | |
| | | if (meshes.length === 1) { |
| | | |
| | | return meshes[0]; |
| | | |
| | | } |
| | | |
| | | var group = new Group(); |
| | | |
| | | for (var i = 0, il = meshes.length; i < il; i++) { |
| | | |
| | | group.add(meshes[i]); |
| | | |
| | | } |
| | | |
| | | return group; |
| | | |
| | | }); |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras |
| | | * @param {number} cameraIndex |
| | | * @return {Promise<THREE.Camera>} |
| | | */ |
| | | GLTFParser.prototype.loadCamera = function (cameraIndex) { |
| | | |
| | | var camera; |
| | | var cameraDef = this.json.cameras[cameraIndex]; |
| | | var params = cameraDef[cameraDef.type]; |
| | | |
| | | if (!params) { |
| | | |
| | | console.warn('THREE.GLTFLoader: Missing camera parameters.'); |
| | | return; |
| | | |
| | | } |
| | | |
| | | if (cameraDef.type === 'perspective') { |
| | | |
| | | camera = new PerspectiveCamera(_Math.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6); |
| | | |
| | | } else if (cameraDef.type === 'orthographic') { |
| | | |
| | | camera = new OrthographicCamera(params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar); |
| | | |
| | | } |
| | | |
| | | if (cameraDef.name !== undefined) camera.name = cameraDef.name; |
| | | |
| | | assignExtrasToUserData(camera, cameraDef); |
| | | |
| | | return Promise.resolve(camera); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins |
| | | * @param {number} skinIndex |
| | | * @return {Promise<Object>} |
| | | */ |
| | | GLTFParser.prototype.loadSkin = function (skinIndex) { |
| | | |
| | | var skinDef = this.json.skins[skinIndex]; |
| | | |
| | | var skinEntry = { joints: skinDef.joints }; |
| | | |
| | | if (skinDef.inverseBindMatrices === undefined) { |
| | | |
| | | return Promise.resolve(skinEntry); |
| | | |
| | | } |
| | | |
| | | return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) { |
| | | |
| | | skinEntry.inverseBindMatrices = accessor; |
| | | |
| | | return skinEntry; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations |
| | | * @param {number} animationIndex |
| | | * @return {Promise<AnimationClip>} |
| | | */ |
| | | GLTFParser.prototype.loadAnimation = function (animationIndex) { |
| | | |
| | | var json = this.json; |
| | | |
| | | var animationDef = json.animations[animationIndex]; |
| | | |
| | | var pendingNodes = []; |
| | | var pendingInputAccessors = []; |
| | | var pendingOutputAccessors = []; |
| | | var pendingSamplers = []; |
| | | var pendingTargets = []; |
| | | |
| | | for (var i = 0, il = animationDef.channels.length; i < il; i++) { |
| | | |
| | | var channel = animationDef.channels[i]; |
| | | var sampler = animationDef.samplers[channel.sampler]; |
| | | var target = channel.target; |
| | | var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. |
| | | var input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input; |
| | | var output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output; |
| | | |
| | | pendingNodes.push(this.getDependency('node', name)); |
| | | pendingInputAccessors.push(this.getDependency('accessor', input)); |
| | | pendingOutputAccessors.push(this.getDependency('accessor', output)); |
| | | pendingSamplers.push(sampler); |
| | | pendingTargets.push(target); |
| | | |
| | | } |
| | | |
| | | return Promise.all([ |
| | | |
| | | Promise.all(pendingNodes), |
| | | Promise.all(pendingInputAccessors), |
| | | Promise.all(pendingOutputAccessors), |
| | | Promise.all(pendingSamplers), |
| | | Promise.all(pendingTargets) |
| | | |
| | | ]).then(function (dependencies) { |
| | | |
| | | var nodes = dependencies[0]; |
| | | var inputAccessors = dependencies[1]; |
| | | var outputAccessors = dependencies[2]; |
| | | var samplers = dependencies[3]; |
| | | var targets = dependencies[4]; |
| | | |
| | | var tracks = []; |
| | | |
| | | for (var i = 0, il = nodes.length; i < il; i++) { |
| | | |
| | | var node = nodes[i]; |
| | | var inputAccessor = inputAccessors[i]; |
| | | var outputAccessor = outputAccessors[i]; |
| | | var sampler = samplers[i]; |
| | | var target = targets[i]; |
| | | |
| | | if (node === undefined) continue; |
| | | |
| | | node.updateMatrix(); |
| | | node.matrixAutoUpdate = true; |
| | | |
| | | var TypedKeyframeTrack; |
| | | |
| | | switch (PATH_PROPERTIES[target.path]) { |
| | | |
| | | case PATH_PROPERTIES.weights: |
| | | |
| | | TypedKeyframeTrack = NumberKeyframeTrack; |
| | | break; |
| | | |
| | | case PATH_PROPERTIES.rotation: |
| | | |
| | | TypedKeyframeTrack = QuaternionKeyframeTrack; |
| | | break; |
| | | |
| | | case PATH_PROPERTIES.position: |
| | | case PATH_PROPERTIES.scale: |
| | | default: |
| | | |
| | | TypedKeyframeTrack = VectorKeyframeTrack; |
| | | break; |
| | | |
| | | } |
| | | |
| | | var targetName = node.name ? node.name : node.uuid; |
| | | |
| | | var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : InterpolateLinear; |
| | | |
| | | var targetNames = []; |
| | | |
| | | if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) { |
| | | |
| | | // Node may be a Group (glTF mesh with several primitives) or a Mesh. |
| | | node.traverse(function (object) { |
| | | |
| | | if (object.isMesh === true && object.morphTargetInfluences) { |
| | | |
| | | targetNames.push(object.name ? object.name : object.uuid); |
| | | |
| | | } |
| | | |
| | | }); |
| | | |
| | | } else { |
| | | |
| | | targetNames.push(targetName); |
| | | |
| | | } |
| | | |
| | | var outputArray = outputAccessor.array; |
| | | |
| | | if (outputAccessor.normalized) { |
| | | |
| | | var scale; |
| | | |
| | | if (outputArray.constructor === Int8Array) { |
| | | |
| | | scale = 1 / 127; |
| | | |
| | | } else if (outputArray.constructor === Uint8Array) { |
| | | |
| | | scale = 1 / 255; |
| | | |
| | | } else if (outputArray.constructor == Int16Array) { |
| | | |
| | | scale = 1 / 32767; |
| | | |
| | | } else if (outputArray.constructor === Uint16Array) { |
| | | |
| | | scale = 1 / 65535; |
| | | |
| | | } else { |
| | | |
| | | throw new Error('THREE.GLTFLoader: Unsupported output accessor component type.'); |
| | | |
| | | } |
| | | |
| | | var scaled = new Float32Array(outputArray.length); |
| | | |
| | | for (var j = 0, jl = outputArray.length; j < jl; j++) { |
| | | |
| | | scaled[j] = outputArray[j] * scale; |
| | | |
| | | } |
| | | |
| | | outputArray = scaled; |
| | | |
| | | } |
| | | |
| | | for (var j = 0, jl = targetNames.length; j < jl; j++) { |
| | | |
| | | var track = new TypedKeyframeTrack( |
| | | targetNames[j] + '.' + PATH_PROPERTIES[target.path], |
| | | inputAccessor.array, |
| | | outputArray, |
| | | interpolation |
| | | ); |
| | | |
| | | // Override interpolation with custom factory method. |
| | | if (sampler.interpolation === 'CUBICSPLINE') { |
| | | |
| | | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline(result) { |
| | | |
| | | // A CUBICSPLINE keyframe in glTF has three output values for each input value, |
| | | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() |
| | | // must be divided by three to get the interpolant's sampleSize argument. |
| | | |
| | | return new GLTFCubicSplineInterpolant(this.times, this.values, this.getValueSize() / 3, result); |
| | | |
| | | }; |
| | | |
| | | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. |
| | | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; |
| | | |
| | | } |
| | | |
| | | tracks.push(track); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex; |
| | | |
| | | return new AnimationClip(name, undefined, tracks); |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy |
| | | * @param {number} nodeIndex |
| | | * @return {Promise<Object3D>} |
| | | */ |
| | | GLTFParser.prototype.loadNode = function (nodeIndex) { |
| | | |
| | | var json = this.json; |
| | | var extensions = this.extensions; |
| | | var parser = this; |
| | | |
| | | var meshReferences = json.meshReferences; |
| | | var meshUses = json.meshUses; |
| | | |
| | | var nodeDef = json.nodes[nodeIndex]; |
| | | |
| | | return (function () { |
| | | |
| | | var pending = []; |
| | | |
| | | if (nodeDef.mesh !== undefined) { |
| | | |
| | | pending.push(parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) { |
| | | |
| | | var node; |
| | | |
| | | if (meshReferences[nodeDef.mesh] > 1) { |
| | | |
| | | var instanceNum = meshUses[nodeDef.mesh]++; |
| | | |
| | | node = mesh.clone(); |
| | | node.name += '_instance_' + instanceNum; |
| | | |
| | | // onBeforeRender copy for Specular-Glossiness |
| | | node.onBeforeRender = mesh.onBeforeRender; |
| | | |
| | | for (var i = 0, il = node.children.length; i < il; i++) { |
| | | |
| | | node.children[i].name += '_instance_' + instanceNum; |
| | | node.children[i].onBeforeRender = mesh.children[i].onBeforeRender; |
| | | |
| | | } |
| | | |
| | | } else { |
| | | |
| | | node = mesh; |
| | | |
| | | } |
| | | |
| | | // if weights are provided on the node, override weights on the mesh. |
| | | if (nodeDef.weights !== undefined) { |
| | | |
| | | node.traverse(function (o) { |
| | | |
| | | if (!o.isMesh) return; |
| | | |
| | | for (var i = 0, il = nodeDef.weights.length; i < il; i++) { |
| | | |
| | | o.morphTargetInfluences[i] = nodeDef.weights[i]; |
| | | |
| | | } |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | return node; |
| | | |
| | | })); |
| | | |
| | | } |
| | | |
| | | if (nodeDef.camera !== undefined) { |
| | | |
| | | pending.push(parser.getDependency('camera', nodeDef.camera)); |
| | | |
| | | } |
| | | |
| | | if (nodeDef.extensions |
| | | && nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL] |
| | | && nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light !== undefined) { |
| | | |
| | | pending.push(parser.getDependency('light', nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light)); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending); |
| | | |
| | | }()).then(function (objects) { |
| | | |
| | | var node; |
| | | |
| | | // .isBone isn't in glTF spec. See .markDefs |
| | | if (nodeDef.isBone === true) { |
| | | |
| | | node = new Bone(); |
| | | |
| | | } else if (objects.length > 1) { |
| | | |
| | | node = new Group(); |
| | | |
| | | } else if (objects.length === 1) { |
| | | |
| | | node = objects[0]; |
| | | |
| | | } else { |
| | | |
| | | node = new Object3D(); |
| | | |
| | | } |
| | | |
| | | if (node !== objects[0]) { |
| | | |
| | | for (var i = 0, il = objects.length; i < il; i++) { |
| | | |
| | | node.add(objects[i]); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (nodeDef.name !== undefined) { |
| | | |
| | | node.userData.name = nodeDef.name; |
| | | node.name = PropertyBinding.sanitizeNodeName(nodeDef.name); |
| | | |
| | | } |
| | | |
| | | assignExtrasToUserData(node, nodeDef); |
| | | |
| | | if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef); |
| | | |
| | | if (nodeDef.matrix !== undefined) { |
| | | |
| | | var matrix = new Matrix4(); |
| | | matrix.fromArray(nodeDef.matrix); |
| | | node.applyMatrix(matrix); |
| | | |
| | | } else { |
| | | |
| | | if (nodeDef.translation !== undefined) { |
| | | |
| | | node.position.fromArray(nodeDef.translation); |
| | | |
| | | } |
| | | |
| | | if (nodeDef.rotation !== undefined) { |
| | | |
| | | node.quaternion.fromArray(nodeDef.rotation); |
| | | |
| | | } |
| | | |
| | | if (nodeDef.scale !== undefined) { |
| | | |
| | | node.scale.fromArray(nodeDef.scale); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return node; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes |
| | | * @param {number} sceneIndex |
| | | * @return {Promise<Scene>} |
| | | */ |
| | | GLTFParser.prototype.loadScene = function () { |
| | | |
| | | // scene node hierachy builder |
| | | |
| | | function buildNodeHierachy(nodeId, parentObject, json, parser) { |
| | | |
| | | var nodeDef = json.nodes[nodeId]; |
| | | |
| | | return parser.getDependency('node', nodeId).then(function (node) { |
| | | |
| | | if (nodeDef.skin === undefined) return node; |
| | | |
| | | // build skeleton here as well |
| | | |
| | | var skinEntry; |
| | | |
| | | return parser.getDependency('skin', nodeDef.skin).then(function (skin) { |
| | | |
| | | skinEntry = skin; |
| | | |
| | | var pendingJoints = []; |
| | | |
| | | for (var i = 0, il = skinEntry.joints.length; i < il; i++) { |
| | | |
| | | pendingJoints.push(parser.getDependency('node', skinEntry.joints[i])); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pendingJoints); |
| | | |
| | | }).then(function (jointNodes) { |
| | | |
| | | node.traverse(function (mesh) { |
| | | |
| | | if (!mesh.isMesh) return; |
| | | |
| | | var bones = []; |
| | | var boneInverses = []; |
| | | |
| | | for (var j = 0, jl = jointNodes.length; j < jl; j++) { |
| | | |
| | | var jointNode = jointNodes[j]; |
| | | |
| | | if (jointNode) { |
| | | |
| | | bones.push(jointNode); |
| | | |
| | | var mat = new Matrix4(); |
| | | |
| | | if (skinEntry.inverseBindMatrices !== undefined) { |
| | | |
| | | mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16); |
| | | |
| | | } |
| | | |
| | | boneInverses.push(mat); |
| | | |
| | | } else { |
| | | |
| | | console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[j]); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | mesh.bind(new Skeleton(bones, boneInverses), mesh.matrixWorld); |
| | | |
| | | }); |
| | | |
| | | return node; |
| | | |
| | | }); |
| | | |
| | | }).then(function (node) { |
| | | |
| | | // build node hierachy |
| | | |
| | | parentObject.add(node); |
| | | |
| | | var pending = []; |
| | | |
| | | if (nodeDef.children) { |
| | | |
| | | var children = nodeDef.children; |
| | | |
| | | for (var i = 0, il = children.length; i < il; i++) { |
| | | |
| | | var child = children[i]; |
| | | pending.push(buildNodeHierachy(child, node, json, parser)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending); |
| | | |
| | | }); |
| | | |
| | | } |
| | | |
| | | return function loadScene(sceneIndex) { |
| | | |
| | | var json = this.json; |
| | | var extensions = this.extensions; |
| | | var sceneDef = this.json.scenes[sceneIndex]; |
| | | var parser = this; |
| | | |
| | | var scene = new Scene(); |
| | | if (sceneDef.name !== undefined) scene.name = sceneDef.name; |
| | | |
| | | assignExtrasToUserData(scene, sceneDef); |
| | | |
| | | if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef); |
| | | |
| | | var nodeIds = sceneDef.nodes || []; |
| | | |
| | | var pending = []; |
| | | |
| | | for (var i = 0, il = nodeIds.length; i < il; i++) { |
| | | |
| | | pending.push(buildNodeHierachy(nodeIds[i], scene, json, parser)); |
| | | |
| | | } |
| | | |
| | | return Promise.all(pending).then(function () { |
| | | |
| | | return scene; |
| | | |
| | | }); |
| | | |
| | | }; |
| | | |
| | | }(); |
| | | |
| | | return GLTFLoader; |
| | | |
| | | } |
| New file |
| | |
| | | /** |
| | | * Loads a Wavefront .mtl file specifying materials |
| | | * |
| | | * @author angelxuanchang |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | Color, |
| | | DefaultLoadingManager, |
| | | FileLoader, |
| | | FrontSide, |
| | | Loader, |
| | | LoaderUtils, |
| | | MeshPhongMaterial, |
| | | RepeatWrapping, |
| | | TextureLoader, |
| | | Vector2 |
| | | } = THREE; |
| | | |
| | | var MTLLoader = function (manager) { |
| | | |
| | | Loader.call(this, manager); |
| | | |
| | | }; |
| | | |
| | | MTLLoader.prototype = Object.assign(Object.create(Loader.prototype), { |
| | | |
| | | constructor: MTLLoader, |
| | | |
| | | /** |
| | | * Loads and parses a MTL asset from a URL. |
| | | * |
| | | * @param {String} url - URL to the MTL file. |
| | | * @param {Function} [onLoad] - Callback invoked with the loaded object. |
| | | * @param {Function} [onProgress] - Callback for download progress. |
| | | * @param {Function} [onError] - Callback for download errors. |
| | | * |
| | | * @see setPath setResourcePath |
| | | * |
| | | * @note In order for relative texture references to resolve correctly |
| | | * you must call setResourcePath() explicitly prior to load. |
| | | */ |
| | | load: function (url, onLoad, onProgress, onError) { |
| | | |
| | | var scope = this; |
| | | |
| | | var path = (this.path === '') ? LoaderUtils.extractUrlBase(url) : this.path; |
| | | |
| | | var loader = new FileLoader(this.manager); |
| | | loader.setPath(this.path); |
| | | loader.load(url, function (text) { |
| | | |
| | | onLoad(scope.parse(text, path)); |
| | | |
| | | }, onProgress, onError); |
| | | |
| | | }, |
| | | |
| | | setMaterialOptions: function (value) { |
| | | |
| | | this.materialOptions = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * Parses a MTL file. |
| | | * |
| | | * @param {String} text - Content of MTL file |
| | | * @return {MTLLoader.MaterialCreator} |
| | | * |
| | | * @see setPath setResourcePath |
| | | * |
| | | * @note In order for relative texture references to resolve correctly |
| | | * you must call setResourcePath() explicitly prior to parse. |
| | | */ |
| | | parse: function (text, path) { |
| | | |
| | | var lines = text.split('\n'); |
| | | var info = {}; |
| | | var delimiter_pattern = /\s+/; |
| | | var materialsInfo = {}; |
| | | |
| | | for (var i = 0; i < lines.length; i++) { |
| | | |
| | | var line = lines[i]; |
| | | line = line.trim(); |
| | | |
| | | if (line.length === 0 || line.charAt(0) === '#') { |
| | | |
| | | // Blank line or comment ignore |
| | | continue; |
| | | |
| | | } |
| | | |
| | | var pos = line.indexOf(' '); |
| | | |
| | | var key = (pos >= 0) ? line.substring(0, pos) : line; |
| | | key = key.toLowerCase(); |
| | | |
| | | var value = (pos >= 0) ? line.substring(pos + 1) : ''; |
| | | value = value.trim(); |
| | | |
| | | if (key === 'newmtl') { |
| | | |
| | | // New material |
| | | |
| | | info = { name: value }; |
| | | materialsInfo[value] = info; |
| | | |
| | | } else { |
| | | |
| | | if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') { |
| | | |
| | | var ss = value.split(delimiter_pattern, 3); |
| | | info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]; |
| | | |
| | | } else { |
| | | |
| | | info[key] = value; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var materialCreator = new MTLLoader.MaterialCreator(this.resourcePath || path, this.materialOptions); |
| | | materialCreator.setCrossOrigin(this.crossOrigin); |
| | | materialCreator.setManager(this.manager); |
| | | materialCreator.setMaterials(materialsInfo); |
| | | return materialCreator; |
| | | |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * Create a new THREE-MTLLoader.MaterialCreator |
| | | * @param baseUrl - Url relative to which textures are loaded |
| | | * @param options - Set of options on how to construct the materials |
| | | * side: Which side to apply the material |
| | | * FrontSide (default), THREE.BackSide, THREE.DoubleSide |
| | | * wrap: What type of wrapping to apply for textures |
| | | * RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping |
| | | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 |
| | | * Default: false, assumed to be already normalized |
| | | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's |
| | | * Default: false |
| | | * @constructor |
| | | */ |
| | | |
| | | MTLLoader.MaterialCreator = function (baseUrl, options) { |
| | | |
| | | this.baseUrl = baseUrl || ''; |
| | | this.options = options; |
| | | this.materialsInfo = {}; |
| | | this.materials = {}; |
| | | this.materialsArray = []; |
| | | this.nameLookup = {}; |
| | | |
| | | this.side = (this.options && this.options.side) ? this.options.side : FrontSide; |
| | | this.wrap = (this.options && this.options.wrap) ? this.options.wrap : RepeatWrapping; |
| | | |
| | | }; |
| | | |
| | | MTLLoader.MaterialCreator.prototype = { |
| | | |
| | | constructor: MTLLoader.MaterialCreator, |
| | | |
| | | crossOrigin: 'anonymous', |
| | | |
| | | setCrossOrigin: function (value) { |
| | | |
| | | this.crossOrigin = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | setManager: function (value) { |
| | | |
| | | this.manager = value; |
| | | |
| | | }, |
| | | |
| | | setMaterials: function (materialsInfo) { |
| | | |
| | | this.materialsInfo = this.convert(materialsInfo); |
| | | this.materials = {}; |
| | | this.materialsArray = []; |
| | | this.nameLookup = {}; |
| | | |
| | | }, |
| | | |
| | | convert: function (materialsInfo) { |
| | | |
| | | if (!this.options) return materialsInfo; |
| | | |
| | | var converted = {}; |
| | | |
| | | for (var mn in materialsInfo) { |
| | | |
| | | // Convert materials info into normalized form based on options |
| | | |
| | | var mat = materialsInfo[mn]; |
| | | |
| | | var covmat = {}; |
| | | |
| | | converted[mn] = covmat; |
| | | |
| | | for (var prop in mat) { |
| | | |
| | | var save = true; |
| | | var value = mat[prop]; |
| | | var lprop = prop.toLowerCase(); |
| | | |
| | | switch (lprop) { |
| | | |
| | | case 'kd': |
| | | case 'ka': |
| | | case 'ks': |
| | | |
| | | // Diffuse color (color under white light) using RGB values |
| | | |
| | | if (this.options && this.options.normalizeRGB) { |
| | | |
| | | value = [value[0] / 255, value[1] / 255, value[2] / 255]; |
| | | |
| | | } |
| | | |
| | | if (this.options && this.options.ignoreZeroRGBs) { |
| | | |
| | | if (value[0] === 0 && value[1] === 0 && value[2] === 0) { |
| | | |
| | | // ignore |
| | | |
| | | save = false; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | default: |
| | | |
| | | break; |
| | | |
| | | } |
| | | |
| | | if (save) { |
| | | |
| | | covmat[lprop] = value; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return converted; |
| | | |
| | | }, |
| | | |
| | | preload: function () { |
| | | |
| | | for (var mn in this.materialsInfo) { |
| | | |
| | | this.create(mn); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | getIndex: function (materialName) { |
| | | |
| | | return this.nameLookup[materialName]; |
| | | |
| | | }, |
| | | |
| | | getAsArray: function () { |
| | | |
| | | var index = 0; |
| | | |
| | | for (var mn in this.materialsInfo) { |
| | | |
| | | this.materialsArray[index] = this.create(mn); |
| | | this.nameLookup[mn] = index; |
| | | index++; |
| | | |
| | | } |
| | | |
| | | return this.materialsArray; |
| | | |
| | | }, |
| | | |
| | | create: function (materialName) { |
| | | |
| | | if (this.materials[materialName] === undefined) { |
| | | |
| | | this.createMaterial_(materialName); |
| | | |
| | | } |
| | | |
| | | return this.materials[materialName]; |
| | | |
| | | }, |
| | | |
| | | createMaterial_: function (materialName) { |
| | | |
| | | // Create material |
| | | |
| | | var scope = this; |
| | | var mat = this.materialsInfo[materialName]; |
| | | var params = { |
| | | |
| | | name: materialName, |
| | | side: this.side |
| | | |
| | | }; |
| | | |
| | | function resolveURL(baseUrl, url) { |
| | | |
| | | if (typeof url !== 'string' || url === '') |
| | | return ''; |
| | | |
| | | // Absolute URL |
| | | if (/^https?:\/\//i.test(url)) return url; |
| | | |
| | | return baseUrl + url; |
| | | |
| | | } |
| | | |
| | | function setMapForType(mapType, value) { |
| | | |
| | | if (params[mapType]) return; // Keep the first encountered texture |
| | | |
| | | var texParams = scope.getTextureParams(value, params); |
| | | var map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url)); |
| | | |
| | | map.repeat.copy(texParams.scale); |
| | | map.offset.copy(texParams.offset); |
| | | |
| | | map.wrapS = scope.wrap; |
| | | map.wrapT = scope.wrap; |
| | | |
| | | params[mapType] = map; |
| | | |
| | | } |
| | | |
| | | for (var prop in mat) { |
| | | |
| | | var value = mat[prop]; |
| | | var n; |
| | | |
| | | if (value === '') continue; |
| | | |
| | | switch (prop.toLowerCase()) { |
| | | |
| | | // Ns is material specular exponent |
| | | |
| | | case 'kd': |
| | | |
| | | // Diffuse color (color under white light) using RGB values |
| | | |
| | | params.color = new Color().fromArray(value); |
| | | |
| | | break; |
| | | |
| | | case 'ks': |
| | | |
| | | // Specular color (color when light is reflected from shiny surface) using RGB values |
| | | params.specular = new Color().fromArray(value); |
| | | |
| | | break; |
| | | |
| | | case 'ke': |
| | | |
| | | // Emissive using RGB values |
| | | params.emissive = new Color().fromArray(value); |
| | | |
| | | break; |
| | | |
| | | case 'map_kd': |
| | | |
| | | // Diffuse texture map |
| | | |
| | | setMapForType("map", value); |
| | | |
| | | break; |
| | | |
| | | case 'map_ks': |
| | | |
| | | // Specular map |
| | | |
| | | setMapForType("specularMap", value); |
| | | |
| | | break; |
| | | |
| | | case 'map_ke': |
| | | |
| | | // Emissive map |
| | | |
| | | setMapForType("emissiveMap", value); |
| | | |
| | | break; |
| | | |
| | | case 'norm': |
| | | |
| | | setMapForType("normalMap", value); |
| | | |
| | | break; |
| | | |
| | | case 'map_bump': |
| | | case 'bump': |
| | | |
| | | // Bump texture map |
| | | |
| | | setMapForType("bumpMap", value); |
| | | |
| | | break; |
| | | |
| | | case 'map_d': |
| | | |
| | | // Alpha map |
| | | |
| | | setMapForType("alphaMap", value); |
| | | params.transparent = true; |
| | | |
| | | break; |
| | | |
| | | case 'ns': |
| | | |
| | | // The specular exponent (defines the focus of the specular highlight) |
| | | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. |
| | | |
| | | params.shininess = parseFloat(value); |
| | | |
| | | break; |
| | | |
| | | case 'd': |
| | | n = parseFloat(value); |
| | | |
| | | if (n < 1) { |
| | | |
| | | params.opacity = n; |
| | | params.transparent = true; |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | case 'tr': |
| | | n = parseFloat(value); |
| | | |
| | | if (this.options && this.options.invertTrProperty) n = 1 - n; |
| | | |
| | | if (n > 0) { |
| | | |
| | | params.opacity = 1 - n; |
| | | params.transparent = true; |
| | | |
| | | } |
| | | |
| | | break; |
| | | |
| | | default: |
| | | break; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | this.materials[materialName] = new MeshPhongMaterial(params); |
| | | return this.materials[materialName]; |
| | | |
| | | }, |
| | | |
| | | getTextureParams: function (value, matParams) { |
| | | |
| | | var texParams = { |
| | | |
| | | scale: new Vector2(1, 1), |
| | | offset: new Vector2(0, 0) |
| | | |
| | | }; |
| | | |
| | | var items = value.split(/\s+/); |
| | | var pos; |
| | | |
| | | pos = items.indexOf('-bm'); |
| | | |
| | | if (pos >= 0) { |
| | | |
| | | matParams.bumpScale = parseFloat(items[pos + 1]); |
| | | items.splice(pos, 2); |
| | | |
| | | } |
| | | |
| | | pos = items.indexOf('-s'); |
| | | |
| | | if (pos >= 0) { |
| | | |
| | | texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); |
| | | items.splice(pos, 4); // we expect 3 parameters here! |
| | | |
| | | } |
| | | |
| | | pos = items.indexOf('-o'); |
| | | |
| | | if (pos >= 0) { |
| | | |
| | | texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); |
| | | items.splice(pos, 4); // we expect 3 parameters here! |
| | | |
| | | } |
| | | |
| | | texParams.url = items.join(' ').trim(); |
| | | return texParams; |
| | | |
| | | }, |
| | | |
| | | loadTexture: function (url, mapping, onLoad, onProgress, onError) { |
| | | |
| | | var texture; |
| | | var manager = (this.manager !== undefined) ? this.manager : DefaultLoadingManager; |
| | | var loader = manager.getHandler(url); |
| | | |
| | | if (loader === null) { |
| | | |
| | | loader = new TextureLoader(manager); |
| | | |
| | | } |
| | | |
| | | if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin); |
| | | texture = loader.load(url, onLoad, onProgress, onError); |
| | | |
| | | if (mapping !== undefined) texture.mapping = mapping; |
| | | |
| | | return texture; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | return { MTLLoader }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author mrdoob / http://mrdoob.com/ |
| | | */ |
| | | |
| | | export default function (THREE) { |
| | | let { |
| | | BufferGeometry, |
| | | FileLoader, |
| | | Float32BufferAttribute, |
| | | Group, |
| | | LineBasicMaterial, |
| | | LineSegments, |
| | | Loader, |
| | | LoaderUtils, |
| | | Material, |
| | | Mesh, |
| | | MeshPhongMaterial, |
| | | NoColors, |
| | | Points, |
| | | PointsMaterial, |
| | | VertexColors |
| | | } = THREE; |
| | | |
| | | |
| | | |
| | | // o object_name | g group_name |
| | | var object_pattern = /^[og]\s*(.+)?/; |
| | | // mtllib file_reference |
| | | var material_library_pattern = /^mtllib /; |
| | | // usemtl material_name |
| | | var material_use_pattern = /^usemtl /; |
| | | |
| | | function ParserState() { |
| | | |
| | | var state = { |
| | | objects: [], |
| | | object: {}, |
| | | |
| | | vertices: [], |
| | | normals: [], |
| | | colors: [], |
| | | uvs: [], |
| | | |
| | | materialLibraries: [], |
| | | |
| | | startObject: function (name, fromDeclaration) { |
| | | |
| | | // If the current object (initial from reset) is not from a g/o declaration in the parsed |
| | | // file. We need to use it for the first parsed g/o to keep things in sync. |
| | | if (this.object && this.object.fromDeclaration === false) { |
| | | |
| | | this.object.name = name; |
| | | this.object.fromDeclaration = (fromDeclaration !== false); |
| | | return; |
| | | |
| | | } |
| | | |
| | | var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined); |
| | | |
| | | if (this.object && typeof this.object._finalize === 'function') { |
| | | |
| | | this.object._finalize(true); |
| | | |
| | | } |
| | | |
| | | this.object = { |
| | | name: name || '', |
| | | fromDeclaration: (fromDeclaration !== false), |
| | | |
| | | geometry: { |
| | | vertices: [], |
| | | normals: [], |
| | | colors: [], |
| | | uvs: [] |
| | | }, |
| | | materials: [], |
| | | smooth: true, |
| | | |
| | | startMaterial: function (name, libraries) { |
| | | |
| | | var previous = this._finalize(false); |
| | | |
| | | // New usemtl declaration overwrites an inherited material, except if faces were declared |
| | | // after the material, then it must be preserved for proper MultiMaterial continuation. |
| | | if (previous && (previous.inherited || previous.groupCount <= 0)) { |
| | | |
| | | this.materials.splice(previous.index, 1); |
| | | |
| | | } |
| | | |
| | | var material = { |
| | | index: this.materials.length, |
| | | name: name || '', |
| | | mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''), |
| | | smooth: (previous !== undefined ? previous.smooth : this.smooth), |
| | | groupStart: (previous !== undefined ? previous.groupEnd : 0), |
| | | groupEnd: - 1, |
| | | groupCount: - 1, |
| | | inherited: false, |
| | | |
| | | clone: function (index) { |
| | | |
| | | var cloned = { |
| | | index: (typeof index === 'number' ? index : this.index), |
| | | name: this.name, |
| | | mtllib: this.mtllib, |
| | | smooth: this.smooth, |
| | | groupStart: 0, |
| | | groupEnd: - 1, |
| | | groupCount: - 1, |
| | | inherited: false |
| | | }; |
| | | cloned.clone = this.clone.bind(cloned); |
| | | return cloned; |
| | | |
| | | } |
| | | }; |
| | | |
| | | this.materials.push(material); |
| | | |
| | | return material; |
| | | |
| | | }, |
| | | |
| | | currentMaterial: function () { |
| | | |
| | | if (this.materials.length > 0) { |
| | | |
| | | return this.materials[this.materials.length - 1]; |
| | | |
| | | } |
| | | |
| | | return undefined; |
| | | |
| | | }, |
| | | |
| | | _finalize: function (end) { |
| | | |
| | | var lastMultiMaterial = this.currentMaterial(); |
| | | if (lastMultiMaterial && lastMultiMaterial.groupEnd === - 1) { |
| | | |
| | | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; |
| | | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; |
| | | lastMultiMaterial.inherited = false; |
| | | |
| | | } |
| | | |
| | | // Ignore objects tail materials if no face declarations followed them before a new o/g started. |
| | | if (end && this.materials.length > 1) { |
| | | |
| | | for (var mi = this.materials.length - 1; mi >= 0; mi--) { |
| | | |
| | | if (this.materials[mi].groupCount <= 0) { |
| | | |
| | | this.materials.splice(mi, 1); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // Guarantee at least one empty material, this makes the creation later more straight forward. |
| | | if (end && this.materials.length === 0) { |
| | | |
| | | this.materials.push({ |
| | | name: '', |
| | | smooth: this.smooth |
| | | }); |
| | | |
| | | } |
| | | |
| | | return lastMultiMaterial; |
| | | |
| | | } |
| | | }; |
| | | |
| | | // Inherit previous objects material. |
| | | // Spec tells us that a declared material must be set to all objects until a new material is declared. |
| | | // If a usemtl declaration is encountered while this new object is being parsed, it will |
| | | // overwrite the inherited material. Exception being that there was already face declarations |
| | | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. |
| | | |
| | | if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') { |
| | | |
| | | var declared = previousMaterial.clone(0); |
| | | declared.inherited = true; |
| | | this.object.materials.push(declared); |
| | | |
| | | } |
| | | |
| | | this.objects.push(this.object); |
| | | |
| | | }, |
| | | |
| | | finalize: function () { |
| | | |
| | | if (this.object && typeof this.object._finalize === 'function') { |
| | | |
| | | this.object._finalize(true); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | parseVertexIndex: function (value, len) { |
| | | |
| | | var index = parseInt(value, 10); |
| | | return (index >= 0 ? index - 1 : index + len / 3) * 3; |
| | | |
| | | }, |
| | | |
| | | parseNormalIndex: function (value, len) { |
| | | |
| | | var index = parseInt(value, 10); |
| | | return (index >= 0 ? index - 1 : index + len / 3) * 3; |
| | | |
| | | }, |
| | | |
| | | parseUVIndex: function (value, len) { |
| | | |
| | | var index = parseInt(value, 10); |
| | | return (index >= 0 ? index - 1 : index + len / 2) * 2; |
| | | |
| | | }, |
| | | |
| | | addVertex: function (a, b, c) { |
| | | |
| | | var src = this.vertices; |
| | | var dst = this.object.geometry.vertices; |
| | | |
| | | dst.push(src[a + 0], src[a + 1], src[a + 2]); |
| | | dst.push(src[b + 0], src[b + 1], src[b + 2]); |
| | | dst.push(src[c + 0], src[c + 1], src[c + 2]); |
| | | |
| | | }, |
| | | |
| | | addVertexPoint: function (a) { |
| | | |
| | | var src = this.vertices; |
| | | var dst = this.object.geometry.vertices; |
| | | |
| | | dst.push(src[a + 0], src[a + 1], src[a + 2]); |
| | | |
| | | }, |
| | | |
| | | addVertexLine: function (a) { |
| | | |
| | | var src = this.vertices; |
| | | var dst = this.object.geometry.vertices; |
| | | |
| | | dst.push(src[a + 0], src[a + 1], src[a + 2]); |
| | | |
| | | }, |
| | | |
| | | addNormal: function (a, b, c) { |
| | | |
| | | var src = this.normals; |
| | | var dst = this.object.geometry.normals; |
| | | |
| | | dst.push(src[a + 0], src[a + 1], src[a + 2]); |
| | | dst.push(src[b + 0], src[b + 1], src[b + 2]); |
| | | dst.push(src[c + 0], src[c + 1], src[c + 2]); |
| | | |
| | | }, |
| | | |
| | | addColor: function (a, b, c) { |
| | | |
| | | var src = this.colors; |
| | | var dst = this.object.geometry.colors; |
| | | |
| | | dst.push(src[a + 0], src[a + 1], src[a + 2]); |
| | | dst.push(src[b + 0], src[b + 1], src[b + 2]); |
| | | dst.push(src[c + 0], src[c + 1], src[c + 2]); |
| | | |
| | | }, |
| | | |
| | | addUV: function (a, b, c) { |
| | | |
| | | var src = this.uvs; |
| | | var dst = this.object.geometry.uvs; |
| | | |
| | | dst.push(src[a + 0], src[a + 1]); |
| | | dst.push(src[b + 0], src[b + 1]); |
| | | dst.push(src[c + 0], src[c + 1]); |
| | | |
| | | }, |
| | | |
| | | addUVLine: function (a) { |
| | | |
| | | var src = this.uvs; |
| | | var dst = this.object.geometry.uvs; |
| | | |
| | | dst.push(src[a + 0], src[a + 1]); |
| | | |
| | | }, |
| | | |
| | | addFace: function (a, b, c, ua, ub, uc, na, nb, nc) { |
| | | |
| | | var vLen = this.vertices.length; |
| | | |
| | | var ia = this.parseVertexIndex(a, vLen); |
| | | var ib = this.parseVertexIndex(b, vLen); |
| | | var ic = this.parseVertexIndex(c, vLen); |
| | | |
| | | this.addVertex(ia, ib, ic); |
| | | |
| | | if (this.colors.length > 0) { |
| | | |
| | | this.addColor(ia, ib, ic); |
| | | |
| | | } |
| | | |
| | | if (ua !== undefined && ua !== '') { |
| | | |
| | | var uvLen = this.uvs.length; |
| | | ia = this.parseUVIndex(ua, uvLen); |
| | | ib = this.parseUVIndex(ub, uvLen); |
| | | ic = this.parseUVIndex(uc, uvLen); |
| | | this.addUV(ia, ib, ic); |
| | | |
| | | } |
| | | |
| | | if (na !== undefined && na !== '') { |
| | | |
| | | // Normals are many times the same. If so, skip function call and parseInt. |
| | | var nLen = this.normals.length; |
| | | ia = this.parseNormalIndex(na, nLen); |
| | | |
| | | ib = na === nb ? ia : this.parseNormalIndex(nb, nLen); |
| | | ic = na === nc ? ia : this.parseNormalIndex(nc, nLen); |
| | | |
| | | this.addNormal(ia, ib, ic); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | addPointGeometry: function (vertices) { |
| | | |
| | | this.object.geometry.type = 'Points'; |
| | | |
| | | var vLen = this.vertices.length; |
| | | |
| | | for (var vi = 0, l = vertices.length; vi < l; vi++) { |
| | | |
| | | this.addVertexPoint(this.parseVertexIndex(vertices[vi], vLen)); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | addLineGeometry: function (vertices, uvs) { |
| | | |
| | | this.object.geometry.type = 'Line'; |
| | | |
| | | var vLen = this.vertices.length; |
| | | var uvLen = this.uvs.length; |
| | | |
| | | for (var vi = 0, l = vertices.length; vi < l; vi++) { |
| | | |
| | | this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen)); |
| | | |
| | | } |
| | | |
| | | for (var uvi = 0, l = uvs.length; uvi < l; uvi++) { |
| | | |
| | | this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen)); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | state.startObject('', false); |
| | | |
| | | return state; |
| | | |
| | | } |
| | | |
| | | // |
| | | |
| | | function OBJLoader(manager) { |
| | | |
| | | Loader.call(this, manager); |
| | | |
| | | this.materials = null; |
| | | |
| | | } |
| | | |
| | | OBJLoader.prototype = Object.assign(Object.create(Loader.prototype), { |
| | | |
| | | constructor: OBJLoader, |
| | | setPath:function(value){ |
| | | this.path = value; |
| | | }, |
| | | |
| | | load: function (url, onLoad, onProgress, onError) { |
| | | |
| | | var scope = this; |
| | | var path = (this.path === '') ? LoaderUtils.extractUrlBase(url) : this.path; |
| | | var loader = new FileLoader(scope.manager); |
| | | loader.setPath(this.path); |
| | | loader.load(url, function (text) { |
| | | |
| | | onLoad(scope.parse(text,path)); |
| | | |
| | | }, onProgress, onError); |
| | | |
| | | }, |
| | | |
| | | setMaterials: function (materials) { |
| | | |
| | | this.materials = materials; |
| | | |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | parse: function (text) { |
| | | |
| | | console.time('OBJLoader'); |
| | | |
| | | var state = new ParserState(); |
| | | |
| | | if (text.indexOf('\r\n') !== - 1) { |
| | | |
| | | // This is faster than String.split with regex that splits on both |
| | | text = text.replace(/\r\n/g, '\n'); |
| | | |
| | | } |
| | | |
| | | if (text.indexOf('\\\n') !== - 1) { |
| | | |
| | | // join lines separated by a line continuation character (\) |
| | | text = text.replace(/\\\n/g, ''); |
| | | |
| | | } |
| | | |
| | | var lines = text.split('\n'); |
| | | var line = '', lineFirstChar = ''; |
| | | var lineLength = 0; |
| | | var result = []; |
| | | |
| | | // Faster to just trim left side of the line. Use if available. |
| | | var trimLeft = (typeof ''.trimLeft === 'function'); |
| | | |
| | | for (var i = 0, l = lines.length; i < l; i++) { |
| | | |
| | | line = lines[i]; |
| | | |
| | | line = trimLeft ? line.trimLeft() : line.trim(); |
| | | |
| | | lineLength = line.length; |
| | | |
| | | if (lineLength === 0) continue; |
| | | |
| | | lineFirstChar = line.charAt(0); |
| | | |
| | | // @todo invoke passed in handler if any |
| | | if (lineFirstChar === '#') continue; |
| | | |
| | | if (lineFirstChar === 'v') { |
| | | // \f -> 匹配一个换页 |
| | | // \n -> 匹配一个换行符 |
| | | // \r -> 匹配一个回车符 |
| | | // \t -> 匹配一个制表符 |
| | | // \v -> 匹配一个垂直制表符 |
| | | // \s+ 表示匹配任意多个上面的字符 |
| | | var data = line.split(/\s+/); |
| | | // var data = line.split(" "); |
| | | |
| | | switch (data[0]) { |
| | | |
| | | case 'v': |
| | | state.vertices.push( |
| | | parseFloat(data[1]), |
| | | parseFloat(data[2]), |
| | | parseFloat(data[3]) |
| | | ); |
| | | if (data.length >= 7) { |
| | | |
| | | state.colors.push( |
| | | parseFloat(data[4]), |
| | | parseFloat(data[5]), |
| | | parseFloat(data[6]) |
| | | |
| | | ); |
| | | |
| | | } |
| | | break; |
| | | case 'vn': |
| | | state.normals.push( |
| | | parseFloat(data[1]), |
| | | parseFloat(data[2]), |
| | | parseFloat(data[3]) |
| | | ); |
| | | break; |
| | | case 'vt': |
| | | state.uvs.push( |
| | | parseFloat(data[1]), |
| | | parseFloat(data[2]) |
| | | ); |
| | | break; |
| | | |
| | | } |
| | | |
| | | } else if (lineFirstChar === 'f') { |
| | | |
| | | var lineData = line.substr(1).trim(); |
| | | var vertexData = lineData.split(/\s+/); |
| | | // var vertexData = lineData.split(" "); |
| | | var faceVertices = []; |
| | | |
| | | // Parse the face vertex data into an easy to work with format |
| | | |
| | | for (var j = 0, jl = vertexData.length; j < jl; j++) { |
| | | |
| | | var vertex = vertexData[j]; |
| | | |
| | | if (vertex.length > 0) { |
| | | |
| | | var vertexParts = vertex.split('/'); |
| | | faceVertices.push(vertexParts); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // Draw an edge between the first vertex and all subsequent vertices to form an n-gon |
| | | |
| | | var v1 = faceVertices[0]; |
| | | |
| | | for (var j = 1, jl = faceVertices.length - 1; j < jl; j++) { |
| | | |
| | | var v2 = faceVertices[j]; |
| | | var v3 = faceVertices[j + 1]; |
| | | |
| | | state.addFace( |
| | | v1[0], v2[0], v3[0], |
| | | v1[1], v2[1], v3[1], |
| | | v1[2], v2[2], v3[2] |
| | | ); |
| | | |
| | | } |
| | | |
| | | } else if (lineFirstChar === 'l') { |
| | | |
| | | var lineParts = line.substring(1).trim().split(" "); |
| | | var lineVertices = [], lineUVs = []; |
| | | |
| | | if (line.indexOf("/") === - 1) { |
| | | |
| | | lineVertices = lineParts; |
| | | |
| | | } else { |
| | | |
| | | for (var li = 0, llen = lineParts.length; li < llen; li++) { |
| | | |
| | | var parts = lineParts[li].split("/"); |
| | | |
| | | if (parts[0] !== "") lineVertices.push(parts[0]); |
| | | if (parts[1] !== "") lineUVs.push(parts[1]); |
| | | |
| | | } |
| | | |
| | | } |
| | | state.addLineGeometry(lineVertices, lineUVs); |
| | | |
| | | } else if (lineFirstChar === 'p') { |
| | | |
| | | var lineData = line.substr(1).trim(); |
| | | var pointData = lineData.split(" "); |
| | | |
| | | state.addPointGeometry(pointData); |
| | | |
| | | } else if ((result = object_pattern.exec(line)) !== null) { |
| | | |
| | | // o object_name |
| | | // or |
| | | // g group_name |
| | | |
| | | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 |
| | | // var name = result[ 0 ].substr( 1 ).trim(); |
| | | var name = (" " + result[0].substr(1).trim()).substr(1); |
| | | |
| | | state.startObject(name); |
| | | |
| | | } else if (material_use_pattern.test(line)) { |
| | | |
| | | // material |
| | | |
| | | state.object.startMaterial(line.substring(7).trim(), state.materialLibraries); |
| | | |
| | | } else if (material_library_pattern.test(line)) { |
| | | |
| | | // mtl file |
| | | |
| | | state.materialLibraries.push(line.substring(7).trim()); |
| | | |
| | | } else if (lineFirstChar === 's') { |
| | | |
| | | result = line.split(' '); |
| | | |
| | | // smooth shading |
| | | |
| | | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, |
| | | // but does not define a usemtl for each face set. |
| | | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). |
| | | // This requires some care to not create extra material on each smooth value for "normal" obj files. |
| | | // where explicit usemtl defines geometry groups. |
| | | // Example asset: examples/models/obj/cerberus/Cerberus.obj |
| | | |
| | | /* |
| | | * http://paulbourke.net/dataformats/obj/ |
| | | * or |
| | | * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf |
| | | * |
| | | * From chapter "Grouping" Syntax explanation "s group_number": |
| | | * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. |
| | | * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form |
| | | * surfaces, smoothing groups are either turned on or off; there is no difference between values greater |
| | | * than 0." |
| | | */ |
| | | if (result.length > 1) { |
| | | |
| | | var value = result[1].trim().toLowerCase(); |
| | | state.object.smooth = (value !== '0' && value !== 'off'); |
| | | |
| | | } else { |
| | | |
| | | // ZBrush can produce "s" lines #11707 |
| | | state.object.smooth = true; |
| | | |
| | | } |
| | | var material = state.object.currentMaterial(); |
| | | if (material) material.smooth = state.object.smooth; |
| | | |
| | | } else { |
| | | |
| | | // Handle null terminated files without exception |
| | | if (line === '\0') continue; |
| | | |
| | | throw new Error('THREE.OBJLoader: Unexpected line: "' + line + '"'); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | state.finalize(); |
| | | |
| | | var container = new Group(); |
| | | container.materialLibraries = [].concat(state.materialLibraries); |
| | | |
| | | for (var i = 0, l = state.objects.length; i < l; i++) { |
| | | |
| | | var object = state.objects[i]; |
| | | var geometry = object.geometry; |
| | | var materials = object.materials; |
| | | var isLine = (geometry.type === 'Line'); |
| | | var isPoints = (geometry.type === 'Points'); |
| | | var hasVertexColors = false; |
| | | |
| | | // Skip o/g line declarations that did not follow with any faces |
| | | if (geometry.vertices.length === 0) continue; |
| | | |
| | | var buffergeometry = new BufferGeometry(); |
| | | |
| | | buffergeometry.setAttribute('position', new Float32BufferAttribute(geometry.vertices, 3)); |
| | | |
| | | if (geometry.normals.length > 0) { |
| | | |
| | | buffergeometry.setAttribute('normal', new Float32BufferAttribute(geometry.normals, 3)); |
| | | |
| | | } else { |
| | | |
| | | buffergeometry.computeVertexNormals(); |
| | | |
| | | } |
| | | |
| | | if (geometry.colors.length > 0) { |
| | | |
| | | hasVertexColors = true; |
| | | buffergeometry.setAttribute('color', new Float32BufferAttribute(geometry.colors, 3)); |
| | | |
| | | } |
| | | |
| | | if (geometry.uvs.length > 0) { |
| | | |
| | | buffergeometry.setAttribute('uv', new Float32BufferAttribute(geometry.uvs, 2)); |
| | | |
| | | } |
| | | |
| | | // Create materials |
| | | |
| | | var createdMaterials = []; |
| | | |
| | | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { |
| | | |
| | | var sourceMaterial = materials[mi]; |
| | | var material = undefined; |
| | | |
| | | if (this.materials !== null) { |
| | | |
| | | material = this.materials.create(sourceMaterial.name); |
| | | |
| | | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. |
| | | if (isLine && material && !(material instanceof LineBasicMaterial)) { |
| | | |
| | | var materialLine = new LineBasicMaterial(); |
| | | Material.prototype.copy.call(materialLine, material); |
| | | materialLine.color.copy(material.color); |
| | | material = materialLine; |
| | | |
| | | } else if (isPoints && material && !(material instanceof PointsMaterial)) { |
| | | |
| | | var materialPoints = new PointsMaterial({ size: 10, sizeAttenuation: false }); |
| | | Material.prototype.copy.call(materialPoints, material); |
| | | materialPoints.color.copy(material.color); |
| | | materialPoints.map = material.map; |
| | | material = materialPoints; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if (!material) { |
| | | |
| | | if (isLine) { |
| | | |
| | | material = new LineBasicMaterial(); |
| | | |
| | | } else if (isPoints) { |
| | | |
| | | material = new PointsMaterial({ size: 1, sizeAttenuation: false }); |
| | | |
| | | } else { |
| | | |
| | | material = new MeshPhongMaterial(); |
| | | |
| | | } |
| | | |
| | | material.name = sourceMaterial.name; |
| | | |
| | | } |
| | | |
| | | material.flatShading = sourceMaterial.smooth ? false : true; |
| | | material.vertexColors = hasVertexColors ? VertexColors : NoColors; |
| | | |
| | | createdMaterials.push(material); |
| | | |
| | | } |
| | | |
| | | // Create mesh |
| | | |
| | | var mesh; |
| | | |
| | | if (createdMaterials.length > 1) { |
| | | |
| | | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { |
| | | |
| | | var sourceMaterial = materials[mi]; |
| | | buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi); |
| | | |
| | | } |
| | | |
| | | if (isLine) { |
| | | |
| | | mesh = new LineSegments(buffergeometry, createdMaterials); |
| | | |
| | | } else if (isPoints) { |
| | | |
| | | mesh = new Points(buffergeometry, createdMaterials); |
| | | |
| | | } else { |
| | | |
| | | mesh = new Mesh(buffergeometry, createdMaterials); |
| | | |
| | | } |
| | | |
| | | } else { |
| | | |
| | | if (isLine) { |
| | | |
| | | mesh = new LineSegments(buffergeometry, createdMaterials[0]); |
| | | |
| | | } else if (isPoints) { |
| | | |
| | | mesh = new Points(buffergeometry, createdMaterials[0]); |
| | | |
| | | } else { |
| | | |
| | | mesh = new Mesh(buffergeometry, createdMaterials[0]); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | mesh.name = object.name; |
| | | |
| | | container.add(mesh); |
| | | |
| | | } |
| | | |
| | | console.timeEnd('OBJLoader'); |
| | | |
| | | return container; |
| | | |
| | | } |
| | | |
| | | }); |
| | | |
| | | return OBJLoader; |
| | | |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author Nikos M. / https://github.com/foo123/ |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | DataTextureLoader, |
| | | FloatType, |
| | | HalfFloatType, |
| | | LinearEncoding, |
| | | LinearFilter, |
| | | NearestFilter, |
| | | RGBEEncoding, |
| | | RGBEFormat, |
| | | RGBFormat, |
| | | UnsignedByteType |
| | | } = THREE; |
| | | |
| | | // https://github.com/mrdoob/three.js/issues/5552 |
| | | // http://en.wikipedia.org/wiki/RGBE_image_format |
| | | |
| | | var RGBELoader = function ( manager ) { |
| | | |
| | | DataTextureLoader.call( this, manager ); |
| | | |
| | | this.type = UnsignedByteType; |
| | | |
| | | }; |
| | | |
| | | RGBELoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype ), { |
| | | |
| | | constructor: RGBELoader, |
| | | |
| | | // adapted from http://www.graphics.cornell.edu/~bjw/rgbe.html |
| | | |
| | | parse: function ( buffer ) { |
| | | |
| | | var |
| | | /* return codes for rgbe routines */ |
| | | //RGBE_RETURN_SUCCESS = 0, |
| | | RGBE_RETURN_FAILURE = - 1, |
| | | |
| | | /* default error routine. change this to change error handling */ |
| | | rgbe_read_error = 1, |
| | | rgbe_write_error = 2, |
| | | rgbe_format_error = 3, |
| | | rgbe_memory_error = 4, |
| | | rgbe_error = function ( rgbe_error_code, msg ) { |
| | | |
| | | switch ( rgbe_error_code ) { |
| | | |
| | | case rgbe_read_error: console.error( "RGBELoader Read Error: " + ( msg || '' ) ); |
| | | break; |
| | | case rgbe_write_error: console.error( "RGBELoader Write Error: " + ( msg || '' ) ); |
| | | break; |
| | | case rgbe_format_error: console.error( "RGBELoader Bad File Format: " + ( msg || '' ) ); |
| | | break; |
| | | default: |
| | | case rgbe_memory_error: console.error( "RGBELoader: Error: " + ( msg || '' ) ); |
| | | |
| | | } |
| | | return RGBE_RETURN_FAILURE; |
| | | |
| | | }, |
| | | |
| | | /* offsets to red, green, and blue components in a data (float) pixel */ |
| | | //RGBE_DATA_RED = 0, |
| | | //RGBE_DATA_GREEN = 1, |
| | | //RGBE_DATA_BLUE = 2, |
| | | |
| | | /* number of floats per pixel, use 4 since stored in rgba image format */ |
| | | //RGBE_DATA_SIZE = 4, |
| | | |
| | | /* flags indicating which fields in an rgbe_header_info are valid */ |
| | | RGBE_VALID_PROGRAMTYPE = 1, |
| | | RGBE_VALID_FORMAT = 2, |
| | | RGBE_VALID_DIMENSIONS = 4, |
| | | |
| | | NEWLINE = "\n", |
| | | |
| | | fgets = function ( buffer, lineLimit, consume ) { |
| | | |
| | | lineLimit = ! lineLimit ? 1024 : lineLimit; |
| | | var p = buffer.pos, |
| | | i = - 1, len = 0, s = '', chunkSize = 128, |
| | | chunk = String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ) |
| | | ; |
| | | while ( ( 0 > ( i = chunk.indexOf( NEWLINE ) ) ) && ( len < lineLimit ) && ( p < buffer.byteLength ) ) { |
| | | |
| | | s += chunk; len += chunk.length; |
| | | p += chunkSize; |
| | | chunk += String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ); |
| | | |
| | | } |
| | | |
| | | if ( - 1 < i ) { |
| | | |
| | | /*for (i=l-1; i>=0; i--) { |
| | | byteCode = m.charCodeAt(i); |
| | | if (byteCode > 0x7f && byteCode <= 0x7ff) byteLen++; |
| | | else if (byteCode > 0x7ff && byteCode <= 0xffff) byteLen += 2; |
| | | if (byteCode >= 0xDC00 && byteCode <= 0xDFFF) i--; //trail surrogate |
| | | }*/ |
| | | if ( false !== consume ) buffer.pos += len + i + 1; |
| | | return s + chunk.slice( 0, i ); |
| | | |
| | | } |
| | | return false; |
| | | |
| | | }, |
| | | |
| | | /* minimal header reading. modify if you want to parse more information */ |
| | | RGBE_ReadHeader = function ( buffer ) { |
| | | |
| | | var line, match, |
| | | |
| | | // regexes to parse header info fields |
| | | magic_token_re = /^#\?(\S+)$/, |
| | | gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/, |
| | | exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/, |
| | | format_re = /^\s*FORMAT=(\S+)\s*$/, |
| | | dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/, |
| | | |
| | | // RGBE format header struct |
| | | header = { |
| | | |
| | | valid: 0, /* indicate which fields are valid */ |
| | | |
| | | string: '', /* the actual header string */ |
| | | |
| | | comments: '', /* comments found in header */ |
| | | |
| | | programtype: 'RGBE', /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */ |
| | | |
| | | format: '', /* RGBE format, default 32-bit_rle_rgbe */ |
| | | |
| | | gamma: 1.0, /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */ |
| | | |
| | | exposure: 1.0, /* a value of 1.0 in an image corresponds to <exposure> watts/steradian/m^2. defaults to 1.0 */ |
| | | |
| | | width: 0, height: 0 /* image dimensions, width/height */ |
| | | |
| | | }; |
| | | |
| | | if ( buffer.pos >= buffer.byteLength || ! ( line = fgets( buffer ) ) ) { |
| | | |
| | | return rgbe_error( rgbe_read_error, "no header found" ); |
| | | |
| | | } |
| | | /* if you want to require the magic token then uncomment the next line */ |
| | | if ( ! ( match = line.match( magic_token_re ) ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "bad initial token" ); |
| | | |
| | | } |
| | | header.valid |= RGBE_VALID_PROGRAMTYPE; |
| | | header.programtype = match[ 1 ]; |
| | | header.string += line + "\n"; |
| | | |
| | | while ( true ) { |
| | | |
| | | line = fgets( buffer ); |
| | | if ( false === line ) break; |
| | | header.string += line + "\n"; |
| | | |
| | | if ( '#' === line.charAt( 0 ) ) { |
| | | |
| | | header.comments += line + "\n"; |
| | | continue; // comment line |
| | | |
| | | } |
| | | |
| | | if ( match = line.match( gamma_re ) ) { |
| | | |
| | | header.gamma = parseFloat( match[ 1 ], 10 ); |
| | | |
| | | } |
| | | if ( match = line.match( exposure_re ) ) { |
| | | |
| | | header.exposure = parseFloat( match[ 1 ], 10 ); |
| | | |
| | | } |
| | | if ( match = line.match( format_re ) ) { |
| | | |
| | | header.valid |= RGBE_VALID_FORMAT; |
| | | header.format = match[ 1 ];//'32-bit_rle_rgbe'; |
| | | |
| | | } |
| | | if ( match = line.match( dimensions_re ) ) { |
| | | |
| | | header.valid |= RGBE_VALID_DIMENSIONS; |
| | | header.height = parseInt( match[ 1 ], 10 ); |
| | | header.width = parseInt( match[ 2 ], 10 ); |
| | | |
| | | } |
| | | |
| | | if ( ( header.valid & RGBE_VALID_FORMAT ) && ( header.valid & RGBE_VALID_DIMENSIONS ) ) break; |
| | | |
| | | } |
| | | |
| | | if ( ! ( header.valid & RGBE_VALID_FORMAT ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "missing format specifier" ); |
| | | |
| | | } |
| | | if ( ! ( header.valid & RGBE_VALID_DIMENSIONS ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "missing image size specifier" ); |
| | | |
| | | } |
| | | |
| | | return header; |
| | | |
| | | }, |
| | | |
| | | RGBE_ReadPixels_RLE = function ( buffer, w, h ) { |
| | | |
| | | var data_rgba, offset, pos, count, byteValue, |
| | | scanline_buffer, ptr, ptr_end, i, l, off, isEncodedRun, |
| | | scanline_width = w, num_scanlines = h, rgbeStart |
| | | ; |
| | | |
| | | if ( |
| | | // run length encoding is not allowed so read flat |
| | | ( ( scanline_width < 8 ) || ( scanline_width > 0x7fff ) ) || |
| | | // this file is not run length encoded |
| | | ( ( 2 !== buffer[ 0 ] ) || ( 2 !== buffer[ 1 ] ) || ( buffer[ 2 ] & 0x80 ) ) |
| | | ) { |
| | | |
| | | // return the flat buffer |
| | | return new Uint8Array( buffer ); |
| | | |
| | | } |
| | | |
| | | if ( scanline_width !== ( ( buffer[ 2 ] << 8 ) | buffer[ 3 ] ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "wrong scanline width" ); |
| | | |
| | | } |
| | | |
| | | data_rgba = new Uint8Array( 4 * w * h ); |
| | | |
| | | if ( ! data_rgba || ! data_rgba.length ) { |
| | | |
| | | return rgbe_error( rgbe_memory_error, "unable to allocate buffer space" ); |
| | | |
| | | } |
| | | |
| | | offset = 0; pos = 0; ptr_end = 4 * scanline_width; |
| | | rgbeStart = new Uint8Array( 4 ); |
| | | scanline_buffer = new Uint8Array( ptr_end ); |
| | | |
| | | // read in each successive scanline |
| | | while ( ( num_scanlines > 0 ) && ( pos < buffer.byteLength ) ) { |
| | | |
| | | if ( pos + 4 > buffer.byteLength ) { |
| | | |
| | | return rgbe_error( rgbe_read_error ); |
| | | |
| | | } |
| | | |
| | | rgbeStart[ 0 ] = buffer[ pos ++ ]; |
| | | rgbeStart[ 1 ] = buffer[ pos ++ ]; |
| | | rgbeStart[ 2 ] = buffer[ pos ++ ]; |
| | | rgbeStart[ 3 ] = buffer[ pos ++ ]; |
| | | |
| | | if ( ( 2 != rgbeStart[ 0 ] ) || ( 2 != rgbeStart[ 1 ] ) || ( ( ( rgbeStart[ 2 ] << 8 ) | rgbeStart[ 3 ] ) != scanline_width ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "bad rgbe scanline format" ); |
| | | |
| | | } |
| | | |
| | | // read each of the four channels for the scanline into the buffer |
| | | // first red, then green, then blue, then exponent |
| | | ptr = 0; |
| | | while ( ( ptr < ptr_end ) && ( pos < buffer.byteLength ) ) { |
| | | |
| | | count = buffer[ pos ++ ]; |
| | | isEncodedRun = count > 128; |
| | | if ( isEncodedRun ) count -= 128; |
| | | |
| | | if ( ( 0 === count ) || ( ptr + count > ptr_end ) ) { |
| | | |
| | | return rgbe_error( rgbe_format_error, "bad scanline data" ); |
| | | |
| | | } |
| | | |
| | | if ( isEncodedRun ) { |
| | | |
| | | // a (encoded) run of the same value |
| | | byteValue = buffer[ pos ++ ]; |
| | | for ( i = 0; i < count; i ++ ) { |
| | | |
| | | scanline_buffer[ ptr ++ ] = byteValue; |
| | | |
| | | } |
| | | //ptr += count; |
| | | |
| | | } else { |
| | | |
| | | // a literal-run |
| | | scanline_buffer.set( buffer.subarray( pos, pos + count ), ptr ); |
| | | ptr += count; pos += count; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | // now convert data from buffer into rgba |
| | | // first red, then green, then blue, then exponent (alpha) |
| | | l = scanline_width; //scanline_buffer.byteLength; |
| | | for ( i = 0; i < l; i ++ ) { |
| | | |
| | | off = 0; |
| | | data_rgba[ offset ] = scanline_buffer[ i + off ]; |
| | | off += scanline_width; //1; |
| | | data_rgba[ offset + 1 ] = scanline_buffer[ i + off ]; |
| | | off += scanline_width; //1; |
| | | data_rgba[ offset + 2 ] = scanline_buffer[ i + off ]; |
| | | off += scanline_width; //1; |
| | | data_rgba[ offset + 3 ] = scanline_buffer[ i + off ]; |
| | | offset += 4; |
| | | |
| | | } |
| | | |
| | | num_scanlines --; |
| | | |
| | | } |
| | | |
| | | return data_rgba; |
| | | |
| | | }; |
| | | |
| | | var RGBEByteToRGBFloat = function ( sourceArray, sourceOffset, destArray, destOffset ) { |
| | | |
| | | var e = sourceArray[ sourceOffset + 3 ]; |
| | | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0; |
| | | |
| | | destArray[ destOffset + 0 ] = sourceArray[ sourceOffset + 0 ] * scale; |
| | | destArray[ destOffset + 1 ] = sourceArray[ sourceOffset + 1 ] * scale; |
| | | destArray[ destOffset + 2 ] = sourceArray[ sourceOffset + 2 ] * scale; |
| | | |
| | | }; |
| | | |
| | | var RGBEByteToRGBHalf = ( function () { |
| | | |
| | | // Source: http://gamedev.stackexchange.com/questions/17326/conversion-of-a-number-from-single-precision-floating-point-representation-to-a/17410#17410 |
| | | |
| | | var floatView = new Float32Array( 1 ); |
| | | var int32View = new Int32Array( floatView.buffer ); |
| | | |
| | | /* This method is faster than the OpenEXR implementation (very often |
| | | * used, eg. in Ogre), with the additional benefit of rounding, inspired |
| | | * by James Tursa?s half-precision code. */ |
| | | function toHalf( val ) { |
| | | |
| | | floatView[ 0 ] = val; |
| | | var x = int32View[ 0 ]; |
| | | |
| | | var bits = ( x >> 16 ) & 0x8000; /* Get the sign */ |
| | | var m = ( x >> 12 ) & 0x07ff; /* Keep one extra bit for rounding */ |
| | | var e = ( x >> 23 ) & 0xff; /* Using int is faster here */ |
| | | |
| | | /* If zero, or denormal, or exponent underflows too much for a denormal |
| | | * half, return signed zero. */ |
| | | if ( e < 103 ) return bits; |
| | | |
| | | /* If NaN, return NaN. If Inf or exponent overflow, return Inf. */ |
| | | if ( e > 142 ) { |
| | | |
| | | bits |= 0x7c00; |
| | | /* If exponent was 0xff and one mantissa bit was set, it means NaN, |
| | | * not Inf, so make sure we set one mantissa bit too. */ |
| | | bits |= ( ( e == 255 ) ? 0 : 1 ) && ( x & 0x007fffff ); |
| | | return bits; |
| | | |
| | | } |
| | | |
| | | /* If exponent underflows but not too much, return a denormal */ |
| | | if ( e < 113 ) { |
| | | |
| | | m |= 0x0800; |
| | | /* Extra rounding may overflow and set mantissa to 0 and exponent |
| | | * to 1, which is OK. */ |
| | | bits |= ( m >> ( 114 - e ) ) + ( ( m >> ( 113 - e ) ) & 1 ); |
| | | return bits; |
| | | |
| | | } |
| | | |
| | | bits |= ( ( e - 112 ) << 10 ) | ( m >> 1 ); |
| | | /* Extra rounding. An overflow will set mantissa to 0 and increment |
| | | * the exponent, which is OK. */ |
| | | bits += m & 1; |
| | | return bits; |
| | | |
| | | } |
| | | |
| | | return function ( sourceArray, sourceOffset, destArray, destOffset ) { |
| | | |
| | | var e = sourceArray[ sourceOffset + 3 ]; |
| | | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0; |
| | | |
| | | destArray[ destOffset + 0 ] = toHalf( sourceArray[ sourceOffset + 0 ] * scale ); |
| | | destArray[ destOffset + 1 ] = toHalf( sourceArray[ sourceOffset + 1 ] * scale ); |
| | | destArray[ destOffset + 2 ] = toHalf( sourceArray[ sourceOffset + 2 ] * scale ); |
| | | |
| | | }; |
| | | |
| | | } )(); |
| | | |
| | | var byteArray = new Uint8Array( buffer ); |
| | | byteArray.pos = 0; |
| | | var rgbe_header_info = RGBE_ReadHeader( byteArray ); |
| | | |
| | | if ( RGBE_RETURN_FAILURE !== rgbe_header_info ) { |
| | | |
| | | var w = rgbe_header_info.width, |
| | | h = rgbe_header_info.height, |
| | | image_rgba_data = RGBE_ReadPixels_RLE( byteArray.subarray( byteArray.pos ), w, h ); |
| | | |
| | | if ( RGBE_RETURN_FAILURE !== image_rgba_data ) { |
| | | |
| | | switch ( this.type ) { |
| | | |
| | | case UnsignedByteType: |
| | | |
| | | var data = image_rgba_data; |
| | | var format = RGBEFormat; // handled as THREE.RGBAFormat in shaders |
| | | var type = UnsignedByteType; |
| | | break; |
| | | |
| | | case FloatType: |
| | | |
| | | var numElements = ( image_rgba_data.length / 4 ) * 3; |
| | | var floatArray = new Float32Array( numElements ); |
| | | |
| | | for ( var j = 0; j < numElements; j ++ ) { |
| | | |
| | | RGBEByteToRGBFloat( image_rgba_data, j * 4, floatArray, j * 3 ); |
| | | |
| | | } |
| | | |
| | | var data = floatArray; |
| | | var format = RGBFormat; |
| | | var type = FloatType; |
| | | break; |
| | | |
| | | case HalfFloatType: |
| | | |
| | | var numElements = ( image_rgba_data.length / 4 ) * 3; |
| | | var halfArray = new Uint16Array( numElements ); |
| | | |
| | | for ( var j = 0; j < numElements; j ++ ) { |
| | | |
| | | RGBEByteToRGBHalf( image_rgba_data, j * 4, halfArray, j * 3 ); |
| | | |
| | | } |
| | | |
| | | var data = halfArray; |
| | | var format = RGBFormat; |
| | | var type = HalfFloatType; |
| | | break; |
| | | |
| | | default: |
| | | |
| | | console.error( 'THREE.RGBELoader: unsupported type: ', this.type ); |
| | | break; |
| | | |
| | | } |
| | | |
| | | return { |
| | | width: w, height: h, |
| | | data: data, |
| | | header: rgbe_header_info.string, |
| | | gamma: rgbe_header_info.gamma, |
| | | exposure: rgbe_header_info.exposure, |
| | | format: format, |
| | | type: type |
| | | }; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return null; |
| | | |
| | | }, |
| | | |
| | | setDataType: function ( value ) { |
| | | |
| | | this.type = value; |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | load: function ( url, onLoad, onProgress, onError ) { |
| | | |
| | | function onLoadCallback( texture, texData ) { |
| | | |
| | | switch ( texture.type ) { |
| | | |
| | | case UnsignedByteType: |
| | | |
| | | texture.encoding = RGBEEncoding; |
| | | texture.minFilter = NearestFilter; |
| | | texture.magFilter = NearestFilter; |
| | | texture.generateMipmaps = false; |
| | | texture.flipY = true; |
| | | break; |
| | | |
| | | case FloatType: |
| | | |
| | | texture.encoding = LinearEncoding; |
| | | texture.minFilter = LinearFilter; |
| | | texture.magFilter = LinearFilter; |
| | | texture.generateMipmaps = false; |
| | | texture.flipY = true; |
| | | break; |
| | | |
| | | case HalfFloatType: |
| | | |
| | | texture.encoding = LinearEncoding; |
| | | texture.minFilter = LinearFilter; |
| | | texture.magFilter = LinearFilter; |
| | | texture.generateMipmaps = false; |
| | | texture.flipY = true; |
| | | break; |
| | | |
| | | } |
| | | |
| | | if ( onLoad ) onLoad( texture, texData ); |
| | | |
| | | } |
| | | |
| | | return DataTextureLoader.prototype.load.call( this, url, onLoadCallback, onProgress, onError ); |
| | | |
| | | } |
| | | |
| | | } ); |
| | | |
| | | return { RGBELoader }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author aleeper / http://adamleeper.com/ |
| | | * @author mrdoob / http://mrdoob.com/ |
| | | * @author gero3 / https://github.com/gero3 |
| | | * @author Mugen87 / https://github.com/Mugen87 |
| | | * @author neverhood311 / https://github.com/neverhood311 |
| | | * |
| | | * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs. |
| | | * |
| | | * Supports both binary and ASCII encoded files, with automatic detection of type. |
| | | * |
| | | * The loader returns a non-indexed buffer geometry. |
| | | * |
| | | * Limitations: |
| | | * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL). |
| | | * There is perhaps some question as to how valid it is to always assume little-endian-ness. |
| | | * ASCII decoding assumes file is UTF-8. |
| | | * |
| | | * Usage: |
| | | * var loader = new STLLoader(); |
| | | * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) { |
| | | * scene.add( new THREE.Mesh( geometry ) ); |
| | | * }); |
| | | * |
| | | * For binary STLs geometry might contain colors for vertices. To use it: |
| | | * // use the same code to load STL as above |
| | | * if (geometry.hasColors) { |
| | | * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors }); |
| | | * } else { .... } |
| | | * var mesh = new THREE.Mesh( geometry, material ); |
| | | * |
| | | * For ASCII STLs containing multiple solids, each solid is assigned to a different group. |
| | | * Groups can be used to assign a different color by defining an array of materials with the same length of |
| | | * geometry.groups and passing it to the Mesh constructor: |
| | | * |
| | | * var mesh = new THREE.Mesh( geometry, material ); |
| | | * |
| | | * For example: |
| | | * |
| | | * var materials = []; |
| | | * var nGeometryGroups = geometry.groups.length; |
| | | * |
| | | * var colorMap = ...; // Some logic to index colors. |
| | | * |
| | | * for (var i = 0; i < nGeometryGroups; i++) { |
| | | * |
| | | * var material = new THREE.MeshPhongMaterial({ |
| | | * color: colorMap[i], |
| | | * wireframe: false |
| | | * }); |
| | | * |
| | | * } |
| | | * |
| | | * materials.push(material); |
| | | * var mesh = new THREE.Mesh(geometry, materials); |
| | | */ |
| | | |
| | | export default function (THREE) { |
| | | let { |
| | | BufferAttribute, |
| | | BufferGeometry, |
| | | FileLoader, |
| | | Float32BufferAttribute, |
| | | Loader, |
| | | LoaderUtils, |
| | | Vector3 |
| | | } = THREE; |
| | | |
| | | |
| | | var STLLoader = function ( manager ) { |
| | | |
| | | Loader.call( this, manager ); |
| | | |
| | | }; |
| | | |
| | | STLLoader.prototype = Object.assign( Object.create( Loader.prototype ), { |
| | | |
| | | constructor: STLLoader, |
| | | |
| | | load: function ( url, onLoad, onProgress, onError ) { |
| | | |
| | | var scope = this; |
| | | |
| | | var loader = new FileLoader( scope.manager ); |
| | | loader.setPath( scope.path ); |
| | | loader.setResponseType( 'arraybuffer' ); |
| | | loader.load( url, function ( text ) { |
| | | |
| | | try { |
| | | |
| | | onLoad( scope.parse( text ) ); |
| | | |
| | | } catch ( exception ) { |
| | | |
| | | if ( onError ) { |
| | | |
| | | onError( exception ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | }, onProgress, onError ); |
| | | |
| | | }, |
| | | |
| | | parse: function ( data ) { |
| | | |
| | | function isBinary( data ) { |
| | | |
| | | var expect, face_size, n_faces, reader; |
| | | reader = new DataView( data ); |
| | | face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 ); |
| | | n_faces = reader.getUint32( 80, true ); |
| | | expect = 80 + ( 32 / 8 ) + ( n_faces * face_size ); |
| | | |
| | | if ( expect === reader.byteLength ) { |
| | | |
| | | return true; |
| | | |
| | | } |
| | | |
| | | // An ASCII STL data must begin with 'solid ' as the first six bytes. |
| | | // However, ASCII STLs lacking the SPACE after the 'd' are known to be |
| | | // plentiful. So, check the first 5 bytes for 'solid'. |
| | | |
| | | // Several encodings, such as UTF-8, precede the text with up to 5 bytes: |
| | | // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding |
| | | // Search for "solid" to start anywhere after those prefixes. |
| | | |
| | | // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd' |
| | | |
| | | var solid = [ 115, 111, 108, 105, 100 ]; |
| | | |
| | | for ( var off = 0; off < 5; off ++ ) { |
| | | |
| | | // If "solid" text is matched to the current offset, declare it to be an ASCII STL. |
| | | |
| | | if ( matchDataViewAt( solid, reader, off ) ) return false; |
| | | |
| | | } |
| | | |
| | | // Couldn't find "solid" text at the beginning; it is binary STL. |
| | | |
| | | return true; |
| | | |
| | | } |
| | | |
| | | function matchDataViewAt( query, reader, offset ) { |
| | | |
| | | // Check if each byte in query matches the corresponding byte from the current offset |
| | | |
| | | for ( var i = 0, il = query.length; i < il; i ++ ) { |
| | | |
| | | if ( query[ i ] !== reader.getUint8( offset + i, false ) ) return false; |
| | | |
| | | } |
| | | |
| | | return true; |
| | | |
| | | } |
| | | |
| | | function parseBinary( data ) { |
| | | |
| | | var reader = new DataView( data ); |
| | | var faces = reader.getUint32( 80, true ); |
| | | |
| | | var r, g, b, hasColors = false, colors; |
| | | var defaultR, defaultG, defaultB, alpha; |
| | | |
| | | // process STL header |
| | | // check for default color in header ("COLOR=rgba" sequence). |
| | | |
| | | for ( var index = 0; index < 80 - 10; index ++ ) { |
| | | |
| | | if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) && |
| | | ( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) && |
| | | ( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) { |
| | | |
| | | hasColors = true; |
| | | colors = new Float32Array( faces * 3 * 3 ); |
| | | |
| | | defaultR = reader.getUint8( index + 6 ) / 255; |
| | | defaultG = reader.getUint8( index + 7 ) / 255; |
| | | defaultB = reader.getUint8( index + 8 ) / 255; |
| | | alpha = reader.getUint8( index + 9 ) / 255; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | var dataOffset = 84; |
| | | var faceLength = 12 * 4 + 2; |
| | | |
| | | var geometry = new BufferGeometry(); |
| | | |
| | | var vertices = new Float32Array( faces * 3 * 3 ); |
| | | var normals = new Float32Array( faces * 3 * 3 ); |
| | | |
| | | for ( var face = 0; face < faces; face ++ ) { |
| | | |
| | | var start = dataOffset + face * faceLength; |
| | | var normalX = reader.getFloat32( start, true ); |
| | | var normalY = reader.getFloat32( start + 4, true ); |
| | | var normalZ = reader.getFloat32( start + 8, true ); |
| | | |
| | | if ( hasColors ) { |
| | | |
| | | var packedColor = reader.getUint16( start + 48, true ); |
| | | |
| | | if ( ( packedColor & 0x8000 ) === 0 ) { |
| | | |
| | | // facet has its own unique color |
| | | |
| | | r = ( packedColor & 0x1F ) / 31; |
| | | g = ( ( packedColor >> 5 ) & 0x1F ) / 31; |
| | | b = ( ( packedColor >> 10 ) & 0x1F ) / 31; |
| | | |
| | | } else { |
| | | |
| | | r = defaultR; |
| | | g = defaultG; |
| | | b = defaultB; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | for ( var i = 1; i <= 3; i ++ ) { |
| | | |
| | | var vertexstart = start + i * 12; |
| | | var componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 ); |
| | | |
| | | vertices[ componentIdx ] = reader.getFloat32( vertexstart, true ); |
| | | vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true ); |
| | | vertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true ); |
| | | |
| | | normals[ componentIdx ] = normalX; |
| | | normals[ componentIdx + 1 ] = normalY; |
| | | normals[ componentIdx + 2 ] = normalZ; |
| | | |
| | | if ( hasColors ) { |
| | | |
| | | colors[ componentIdx ] = r; |
| | | colors[ componentIdx + 1 ] = g; |
| | | colors[ componentIdx + 2 ] = b; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | geometry.addAttribute( 'position', new BufferAttribute( vertices, 3 ) ); |
| | | geometry.addAttribute( 'normal', new BufferAttribute( normals, 3 ) ); |
| | | |
| | | if ( hasColors ) { |
| | | |
| | | geometry.addAttribute( 'color', new BufferAttribute( colors, 3 ) ); |
| | | geometry.hasColors = true; |
| | | geometry.alpha = alpha; |
| | | |
| | | } |
| | | |
| | | return geometry; |
| | | |
| | | } |
| | | |
| | | function parseASCII( data ) { |
| | | |
| | | var geometry = new BufferGeometry(); |
| | | var patternSolid = /solid([\s\S]*?)endsolid/g; |
| | | var patternFace = /facet([\s\S]*?)endfacet/g; |
| | | var faceCounter = 0; |
| | | |
| | | var patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source; |
| | | var patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' ); |
| | | var patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' ); |
| | | |
| | | var vertices = []; |
| | | var normals = []; |
| | | |
| | | var normal = new Vector3(); |
| | | |
| | | var result; |
| | | |
| | | var groupVertexes = []; |
| | | var groupCount = 0; |
| | | var startVertex = 0; |
| | | var endVertex = 0; |
| | | |
| | | while ( ( result = patternSolid.exec( data ) ) !== null ) { |
| | | |
| | | startVertex = endVertex; |
| | | |
| | | var solid = result[ 0 ]; |
| | | |
| | | while ( ( result = patternFace.exec( solid ) ) !== null ) { |
| | | |
| | | var vertexCountPerFace = 0; |
| | | var normalCountPerFace = 0; |
| | | |
| | | var text = result[ 0 ]; |
| | | |
| | | while ( ( result = patternNormal.exec( text ) ) !== null ) { |
| | | |
| | | normal.x = parseFloat( result[ 1 ] ); |
| | | normal.y = parseFloat( result[ 2 ] ); |
| | | normal.z = parseFloat( result[ 3 ] ); |
| | | normalCountPerFace ++; |
| | | |
| | | } |
| | | |
| | | while ( ( result = patternVertex.exec( text ) ) !== null ) { |
| | | |
| | | vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) ); |
| | | normals.push( normal.x, normal.y, normal.z ); |
| | | vertexCountPerFace ++; |
| | | endVertex ++; |
| | | |
| | | } |
| | | |
| | | // every face have to own ONE valid normal |
| | | |
| | | if ( normalCountPerFace !== 1 ) { |
| | | |
| | | console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter ); |
| | | |
| | | } |
| | | |
| | | // each face have to own THREE valid vertices |
| | | |
| | | if ( vertexCountPerFace !== 3 ) { |
| | | |
| | | console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter ); |
| | | |
| | | } |
| | | |
| | | faceCounter ++; |
| | | |
| | | } |
| | | |
| | | groupVertexes.push( { startVertex: startVertex, endVertex: endVertex } ); |
| | | groupCount ++; |
| | | |
| | | } |
| | | |
| | | geometry.addAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); |
| | | geometry.addAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); |
| | | |
| | | if ( groupCount > 0 ) { |
| | | |
| | | for ( var i = 0; i < groupVertexes.length; i ++ ) { |
| | | |
| | | geometry.addGroup( groupVertexes[ i ].startVertex, groupVertexes[ i ].endVertex, i ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return geometry; |
| | | |
| | | } |
| | | |
| | | function ensureString( buffer ) { |
| | | |
| | | if ( typeof buffer !== 'string' ) { |
| | | |
| | | return LoaderUtils.decodeText( new Uint8Array( buffer ) ); |
| | | |
| | | } |
| | | |
| | | return buffer; |
| | | |
| | | } |
| | | |
| | | function ensureBinary( buffer ) { |
| | | |
| | | if ( typeof buffer === 'string' ) { |
| | | |
| | | var array_buffer = new Uint8Array( buffer.length ); |
| | | for ( var i = 0; i < buffer.length; i ++ ) { |
| | | |
| | | array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian |
| | | |
| | | } |
| | | |
| | | return array_buffer.buffer || array_buffer; |
| | | |
| | | } else { |
| | | |
| | | return buffer; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | // start |
| | | |
| | | var binData = ensureBinary( data ); |
| | | |
| | | return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) ); |
| | | |
| | | } |
| | | |
| | | } ); |
| | | |
| | | return { STLLoader }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author Prashant Sharma / spidersharma03 |
| | | * @author Ben Houston / bhouston, https://clara.io |
| | | * |
| | | * This class takes the cube lods(corresponding to different roughness values), and creates a single cubeUV |
| | | * Texture. The format for a given roughness set of faces is simply:: |
| | | * +X+Y+Z |
| | | * -X-Y-Z |
| | | * For every roughness a mip map chain is also saved, which is essential to remove the texture artifacts due to |
| | | * minification. |
| | | * Right now for every face a PlaneMesh is drawn, which leads to a lot of geometry draw calls, but can be replaced |
| | | * later by drawing a single buffer and by sending the appropriate faceIndex via vertex attributes. |
| | | * The arrangement of the faces is fixed, as assuming this arrangement, the sampling function has been written. |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | BackSide, |
| | | CubeUVReflectionMapping, |
| | | LinearFilter, |
| | | LinearToneMapping, |
| | | Mesh, |
| | | NoBlending, |
| | | OrthographicCamera, |
| | | PlaneBufferGeometry, |
| | | RGBEEncoding, |
| | | RGBM16Encoding, |
| | | Scene, |
| | | ShaderMaterial, |
| | | Vector2, |
| | | Vector3, |
| | | WebGLRenderTarget |
| | | } = THREE; |
| | | |
| | | var PMREMCubeUVPacker = ( function () { |
| | | |
| | | var camera = new OrthographicCamera(); |
| | | var scene = new Scene(); |
| | | var shader = getShader(); |
| | | |
| | | var PMREMCubeUVPacker = function ( cubeTextureLods ) { |
| | | |
| | | this.cubeLods = cubeTextureLods; |
| | | var size = cubeTextureLods[ 0 ].width * 4; |
| | | |
| | | var sourceTexture = cubeTextureLods[ 0 ].texture; |
| | | var params = { |
| | | format: sourceTexture.format, |
| | | magFilter: sourceTexture.magFilter, |
| | | minFilter: sourceTexture.minFilter, |
| | | type: sourceTexture.type, |
| | | generateMipmaps: sourceTexture.generateMipmaps, |
| | | anisotropy: sourceTexture.anisotropy, |
| | | encoding: ( sourceTexture.encoding === RGBEEncoding ) ? RGBM16Encoding : sourceTexture.encoding |
| | | }; |
| | | |
| | | if ( params.encoding === RGBM16Encoding ) { |
| | | |
| | | params.magFilter = LinearFilter; |
| | | params.minFilter = LinearFilter; |
| | | |
| | | } |
| | | |
| | | this.CubeUVRenderTarget = new WebGLRenderTarget( size, size, params ); |
| | | this.CubeUVRenderTarget.texture.name = "PMREMCubeUVPacker.cubeUv"; |
| | | this.CubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping; |
| | | |
| | | this.objects = []; |
| | | |
| | | var geometry = new PlaneBufferGeometry( 1, 1 ); |
| | | |
| | | var faceOffsets = []; |
| | | faceOffsets.push( new Vector2( 0, 0 ) ); |
| | | faceOffsets.push( new Vector2( 1, 0 ) ); |
| | | faceOffsets.push( new Vector2( 2, 0 ) ); |
| | | faceOffsets.push( new Vector2( 0, 1 ) ); |
| | | faceOffsets.push( new Vector2( 1, 1 ) ); |
| | | faceOffsets.push( new Vector2( 2, 1 ) ); |
| | | |
| | | var textureResolution = size; |
| | | size = cubeTextureLods[ 0 ].width; |
| | | |
| | | var offset2 = 0; |
| | | var c = 4.0; |
| | | this.numLods = Math.log( cubeTextureLods[ 0 ].width ) / Math.log( 2 ) - 2; // IE11 doesn't support Math.log2 |
| | | for ( var i = 0; i < this.numLods; i ++ ) { |
| | | |
| | | var offset1 = ( textureResolution - textureResolution / c ) * 0.5; |
| | | if ( size > 16 ) c *= 2; |
| | | var nMips = size > 16 ? 6 : 1; |
| | | var mipOffsetX = 0; |
| | | var mipOffsetY = 0; |
| | | var mipSize = size; |
| | | |
| | | for ( var j = 0; j < nMips; j ++ ) { |
| | | |
| | | // Mip Maps |
| | | for ( var k = 0; k < 6; k ++ ) { |
| | | |
| | | // 6 Cube Faces |
| | | var material = shader.clone(); |
| | | material.uniforms[ 'envMap' ].value = this.cubeLods[ i ].texture; |
| | | material.envMap = this.cubeLods[ i ].texture; |
| | | material.uniforms[ 'faceIndex' ].value = k; |
| | | material.uniforms[ 'mapSize' ].value = mipSize; |
| | | |
| | | var planeMesh = new Mesh( geometry, material ); |
| | | planeMesh.position.x = faceOffsets[ k ].x * mipSize - offset1 + mipOffsetX; |
| | | planeMesh.position.y = faceOffsets[ k ].y * mipSize - offset1 + offset2 + mipOffsetY; |
| | | planeMesh.material.side = BackSide; |
| | | planeMesh.scale.setScalar( mipSize ); |
| | | this.objects.push( planeMesh ); |
| | | |
| | | } |
| | | mipOffsetY += 1.75 * mipSize; |
| | | mipOffsetX += 1.25 * mipSize; |
| | | mipSize /= 2; |
| | | |
| | | } |
| | | offset2 += 2 * size; |
| | | if ( size > 16 ) size /= 2; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | PMREMCubeUVPacker.prototype = { |
| | | |
| | | constructor: PMREMCubeUVPacker, |
| | | |
| | | update: function ( renderer ) { |
| | | |
| | | var size = this.cubeLods[ 0 ].width * 4; |
| | | // top and bottom are swapped for some reason? |
| | | camera.left = - size * 0.5; |
| | | camera.right = size * 0.5; |
| | | camera.top = - size * 0.5; |
| | | camera.bottom = size * 0.5; |
| | | camera.near = 0; |
| | | camera.far = 1; |
| | | camera.updateProjectionMatrix(); |
| | | |
| | | for ( var i = 0; i < this.objects.length; i ++ ) { |
| | | |
| | | scene.add( this.objects[ i ] ); |
| | | |
| | | } |
| | | |
| | | var gammaInput = renderer.gammaInput; |
| | | var gammaOutput = renderer.gammaOutput; |
| | | var toneMapping = renderer.toneMapping; |
| | | var toneMappingExposure = renderer.toneMappingExposure; |
| | | var currentRenderTarget = renderer.getRenderTarget(); |
| | | |
| | | renderer.gammaInput = false; |
| | | renderer.gammaOutput = false; |
| | | renderer.toneMapping = LinearToneMapping; |
| | | renderer.toneMappingExposure = 1.0; |
| | | renderer.setRenderTarget( this.CubeUVRenderTarget ); |
| | | renderer.render( scene, camera ); |
| | | |
| | | renderer.setRenderTarget( currentRenderTarget ); |
| | | renderer.toneMapping = toneMapping; |
| | | renderer.toneMappingExposure = toneMappingExposure; |
| | | renderer.gammaInput = gammaInput; |
| | | renderer.gammaOutput = gammaOutput; |
| | | |
| | | for ( var i = 0; i < this.objects.length; i ++ ) { |
| | | |
| | | scene.remove( this.objects[ i ] ); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | dispose: function () { |
| | | |
| | | for ( var i = 0, l = this.objects.length; i < l; i ++ ) { |
| | | |
| | | this.objects[ i ].material.dispose(); |
| | | |
| | | } |
| | | |
| | | this.objects[ 0 ].geometry.dispose(); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | function getShader() { |
| | | |
| | | var shaderMaterial = new ShaderMaterial( { |
| | | |
| | | uniforms: { |
| | | "faceIndex": { value: 0 }, |
| | | "mapSize": { value: 0 }, |
| | | "envMap": { value: null }, |
| | | "testColor": { value: new Vector3( 1, 1, 1 ) } |
| | | }, |
| | | |
| | | vertexShader: |
| | | "precision highp float;\ |
| | | varying vec2 vUv;\ |
| | | void main() {\ |
| | | vUv = uv;\ |
| | | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\ |
| | | }", |
| | | |
| | | fragmentShader: |
| | | "precision highp float;\ |
| | | varying vec2 vUv;\ |
| | | uniform samplerCube envMap;\ |
| | | uniform float mapSize;\ |
| | | uniform vec3 testColor;\ |
| | | uniform int faceIndex;\ |
| | | \ |
| | | void main() {\ |
| | | vec3 sampleDirection;\ |
| | | vec2 uv = vUv;\ |
| | | uv = uv * 2.0 - 1.0;\ |
| | | uv.y *= -1.0;\ |
| | | if(faceIndex == 0) {\ |
| | | sampleDirection = normalize(vec3(1.0, uv.y, -uv.x));\ |
| | | } else if(faceIndex == 1) {\ |
| | | sampleDirection = normalize(vec3(uv.x, 1.0, uv.y));\ |
| | | } else if(faceIndex == 2) {\ |
| | | sampleDirection = normalize(vec3(uv.x, uv.y, 1.0));\ |
| | | } else if(faceIndex == 3) {\ |
| | | sampleDirection = normalize(vec3(-1.0, uv.y, uv.x));\ |
| | | } else if(faceIndex == 4) {\ |
| | | sampleDirection = normalize(vec3(uv.x, -1.0, -uv.y));\ |
| | | } else {\ |
| | | sampleDirection = normalize(vec3(-uv.x, uv.y, -1.0));\ |
| | | }\ |
| | | vec4 color = envMapTexelToLinear( textureCube( envMap, sampleDirection ) );\ |
| | | gl_FragColor = linearToOutputTexel( color );\ |
| | | }", |
| | | |
| | | blending: NoBlending |
| | | |
| | | } ); |
| | | |
| | | shaderMaterial.type = 'PMREMCubeUVPacker'; |
| | | |
| | | return shaderMaterial; |
| | | |
| | | } |
| | | |
| | | |
| | | return PMREMCubeUVPacker; |
| | | |
| | | } )(); |
| | | |
| | | return { PMREMCubeUVPacker }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author Prashant Sharma / spidersharma03 |
| | | * @author Ben Houston / bhouston, https://clara.io |
| | | * |
| | | * To avoid cube map seams, I create an extra pixel around each face. This way when the cube map is |
| | | * sampled by an application later(with a little care by sampling the centre of the texel), the extra 1 border |
| | | * of pixels makes sure that there is no seams artifacts present. This works perfectly for cubeUV format as |
| | | * well where the 6 faces can be arranged in any manner whatsoever. |
| | | * Code in the beginning of fragment shader's main function does this job for a given resolution. |
| | | * Run Scene_PMREM_Test.html in the examples directory to see the sampling from the cube lods generated |
| | | * by this class. |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | DoubleSide, |
| | | GammaEncoding, |
| | | LinearEncoding, |
| | | LinearFilter, |
| | | LinearToneMapping, |
| | | Mesh, |
| | | NearestFilter, |
| | | NoBlending, |
| | | OrthographicCamera, |
| | | PlaneBufferGeometry, |
| | | Scene, |
| | | ShaderMaterial, |
| | | WebGLRenderTargetCube, |
| | | sRGBEncoding |
| | | } = THREE; |
| | | |
| | | var PMREMGenerator = ( function () { |
| | | |
| | | var shader = getShader(); |
| | | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0.0, 1000 ); |
| | | var scene = new Scene(); |
| | | var planeMesh = new Mesh( new PlaneBufferGeometry( 2, 2, 0 ), shader ); |
| | | planeMesh.material.side = DoubleSide; |
| | | scene.add( planeMesh ); |
| | | scene.add( camera ); |
| | | |
| | | var PMREMGenerator = function ( sourceTexture, samplesPerLevel, resolution ) { |
| | | |
| | | this.sourceTexture = sourceTexture; |
| | | this.resolution = ( resolution !== undefined ) ? resolution : 256; // NODE: 256 is currently hard coded in the glsl code for performance reasons |
| | | this.samplesPerLevel = ( samplesPerLevel !== undefined ) ? samplesPerLevel : 32; |
| | | |
| | | var monotonicEncoding = ( this.sourceTexture.encoding === LinearEncoding ) || |
| | | ( this.sourceTexture.encoding === GammaEncoding ) || ( this.sourceTexture.encoding === sRGBEncoding ); |
| | | |
| | | this.sourceTexture.minFilter = ( monotonicEncoding ) ? LinearFilter : NearestFilter; |
| | | this.sourceTexture.magFilter = ( monotonicEncoding ) ? LinearFilter : NearestFilter; |
| | | this.sourceTexture.generateMipmaps = this.sourceTexture.generateMipmaps && monotonicEncoding; |
| | | |
| | | this.cubeLods = []; |
| | | |
| | | var size = this.resolution; |
| | | var params = { |
| | | format: this.sourceTexture.format, |
| | | magFilter: this.sourceTexture.magFilter, |
| | | minFilter: this.sourceTexture.minFilter, |
| | | type: this.sourceTexture.type, |
| | | generateMipmaps: this.sourceTexture.generateMipmaps, |
| | | anisotropy: this.sourceTexture.anisotropy, |
| | | encoding: this.sourceTexture.encoding |
| | | }; |
| | | |
| | | // how many LODs fit in the given CubeUV Texture. |
| | | this.numLods = Math.log( size ) / Math.log( 2 ) - 2; // IE11 doesn't support Math.log2 |
| | | |
| | | for ( var i = 0; i < this.numLods; i ++ ) { |
| | | |
| | | var renderTarget = new WebGLRenderTargetCube( size, size, params ); |
| | | renderTarget.texture.name = "PMREMGenerator.cube" + i; |
| | | this.cubeLods.push( renderTarget ); |
| | | size = Math.max( 16, size / 2 ); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | PMREMGenerator.prototype = { |
| | | |
| | | constructor: PMREMGenerator, |
| | | |
| | | /* |
| | | * Prashant Sharma / spidersharma03: More thought and work is needed here. |
| | | * Right now it's a kind of a hack to use the previously convolved map to convolve the current one. |
| | | * I tried to use the original map to convolve all the lods, but for many textures(specially the high frequency) |
| | | * even a high number of samples(1024) dosen't lead to satisfactory results. |
| | | * By using the previous convolved maps, a lower number of samples are generally sufficient(right now 32, which |
| | | * gives okay results unless we see the reflection very carefully, or zoom in too much), however the math |
| | | * goes wrong as the distribution function tries to sample a larger area than what it should be. So I simply scaled |
| | | * the roughness by 0.9(totally empirical) to try to visually match the original result. |
| | | * The condition "if(i <5)" is also an attemt to make the result match the original result. |
| | | * This method requires the most amount of thinking I guess. Here is a paper which we could try to implement in future:: |
| | | * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html |
| | | */ |
| | | update: function ( renderer ) { |
| | | |
| | | // Texture should only be flipped for CubeTexture, not for |
| | | // a Texture created via WebGLRenderTargetCube. |
| | | var tFlip = ( this.sourceTexture.isCubeTexture ) ? - 1 : 1; |
| | | |
| | | shader.defines[ 'SAMPLES_PER_LEVEL' ] = this.samplesPerLevel; |
| | | shader.uniforms[ 'faceIndex' ].value = 0; |
| | | shader.uniforms[ 'envMap' ].value = this.sourceTexture; |
| | | shader.envMap = this.sourceTexture; |
| | | shader.needsUpdate = true; |
| | | |
| | | var gammaInput = renderer.gammaInput; |
| | | var gammaOutput = renderer.gammaOutput; |
| | | var toneMapping = renderer.toneMapping; |
| | | var toneMappingExposure = renderer.toneMappingExposure; |
| | | var currentRenderTarget = renderer.getRenderTarget(); |
| | | |
| | | renderer.toneMapping = LinearToneMapping; |
| | | renderer.toneMappingExposure = 1.0; |
| | | renderer.gammaInput = false; |
| | | renderer.gammaOutput = false; |
| | | |
| | | for ( var i = 0; i < this.numLods; i ++ ) { |
| | | |
| | | var r = i / ( this.numLods - 1 ); |
| | | shader.uniforms[ 'roughness' ].value = r * 0.9; // see comment above, pragmatic choice |
| | | // Only apply the tFlip for the first LOD |
| | | shader.uniforms[ 'tFlip' ].value = ( i == 0 ) ? tFlip : 1; |
| | | var size = this.cubeLods[ i ].width; |
| | | shader.uniforms[ 'mapSize' ].value = size; |
| | | this.renderToCubeMapTarget( renderer, this.cubeLods[ i ] ); |
| | | |
| | | if ( i < 5 ) shader.uniforms[ 'envMap' ].value = this.cubeLods[ i ].texture; |
| | | |
| | | } |
| | | |
| | | renderer.setRenderTarget( currentRenderTarget ); |
| | | renderer.toneMapping = toneMapping; |
| | | renderer.toneMappingExposure = toneMappingExposure; |
| | | renderer.gammaInput = gammaInput; |
| | | renderer.gammaOutput = gammaOutput; |
| | | |
| | | }, |
| | | |
| | | renderToCubeMapTarget: function ( renderer, renderTarget ) { |
| | | |
| | | for ( var i = 0; i < 6; i ++ ) { |
| | | |
| | | this.renderToCubeMapTargetFace( renderer, renderTarget, i ); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | renderToCubeMapTargetFace: function ( renderer, renderTarget, faceIndex ) { |
| | | |
| | | shader.uniforms[ 'faceIndex' ].value = faceIndex; |
| | | renderer.setRenderTarget( renderTarget, faceIndex ); |
| | | renderer.clear(); |
| | | renderer.render( scene, camera ); |
| | | |
| | | }, |
| | | |
| | | dispose: function () { |
| | | |
| | | for ( var i = 0, l = this.cubeLods.length; i < l; i ++ ) { |
| | | |
| | | this.cubeLods[ i ].dispose(); |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | }; |
| | | |
| | | function getShader() { |
| | | |
| | | var shaderMaterial = new ShaderMaterial( { |
| | | |
| | | defines: { |
| | | "SAMPLES_PER_LEVEL": 20, |
| | | }, |
| | | |
| | | uniforms: { |
| | | "faceIndex": { value: 0 }, |
| | | "roughness": { value: 0.5 }, |
| | | "mapSize": { value: 0.5 }, |
| | | "envMap": { value: null }, |
| | | "tFlip": { value: - 1 }, |
| | | }, |
| | | |
| | | vertexShader: |
| | | "varying vec2 vUv;\n\ |
| | | void main() {\n\ |
| | | vUv = uv;\n\ |
| | | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\ |
| | | }", |
| | | |
| | | fragmentShader: |
| | | "#include <common>\n\ |
| | | varying vec2 vUv;\n\ |
| | | uniform int faceIndex;\n\ |
| | | uniform float roughness;\n\ |
| | | uniform samplerCube envMap;\n\ |
| | | uniform float mapSize;\n\ |
| | | uniform float tFlip;\n\ |
| | | \n\ |
| | | float GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {\n\ |
| | | float a = ggxRoughness + 0.0001;\n\ |
| | | a *= a;\n\ |
| | | return ( 2.0 / a - 2.0 );\n\ |
| | | }\n\ |
| | | vec3 ImportanceSamplePhong(vec2 uv, mat3 vecSpace, float specPow) {\n\ |
| | | float phi = uv.y * 2.0 * PI;\n\ |
| | | float cosTheta = pow(1.0 - uv.x, 1.0 / (specPow + 1.0));\n\ |
| | | float sinTheta = sqrt(1.0 - cosTheta * cosTheta);\n\ |
| | | vec3 sampleDir = vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);\n\ |
| | | return vecSpace * sampleDir;\n\ |
| | | }\n\ |
| | | vec3 ImportanceSampleGGX( vec2 uv, mat3 vecSpace, float Roughness )\n\ |
| | | {\n\ |
| | | float a = Roughness * Roughness;\n\ |
| | | float Phi = 2.0 * PI * uv.x;\n\ |
| | | float CosTheta = sqrt( (1.0 - uv.y) / ( 1.0 + (a*a - 1.0) * uv.y ) );\n\ |
| | | float SinTheta = sqrt( 1.0 - CosTheta * CosTheta );\n\ |
| | | return vecSpace * vec3(SinTheta * cos( Phi ), SinTheta * sin( Phi ), CosTheta);\n\ |
| | | }\n\ |
| | | mat3 matrixFromVector(vec3 n) {\n\ |
| | | float a = 1.0 / (1.0 + n.z);\n\ |
| | | float b = -n.x * n.y * a;\n\ |
| | | vec3 b1 = vec3(1.0 - n.x * n.x * a, b, -n.x);\n\ |
| | | vec3 b2 = vec3(b, 1.0 - n.y * n.y * a, -n.y);\n\ |
| | | return mat3(b1, b2, n);\n\ |
| | | }\n\ |
| | | \n\ |
| | | vec4 testColorMap(float Roughness) {\n\ |
| | | vec4 color;\n\ |
| | | if(faceIndex == 0)\n\ |
| | | color = vec4(1.0,0.0,0.0,1.0);\n\ |
| | | else if(faceIndex == 1)\n\ |
| | | color = vec4(0.0,1.0,0.0,1.0);\n\ |
| | | else if(faceIndex == 2)\n\ |
| | | color = vec4(0.0,0.0,1.0,1.0);\n\ |
| | | else if(faceIndex == 3)\n\ |
| | | color = vec4(1.0,1.0,0.0,1.0);\n\ |
| | | else if(faceIndex == 4)\n\ |
| | | color = vec4(0.0,1.0,1.0,1.0);\n\ |
| | | else\n\ |
| | | color = vec4(1.0,0.0,1.0,1.0);\n\ |
| | | color *= ( 1.0 - Roughness );\n\ |
| | | return color;\n\ |
| | | }\n\ |
| | | void main() {\n\ |
| | | vec3 sampleDirection;\n\ |
| | | vec2 uv = vUv*2.0 - 1.0;\n\ |
| | | float offset = -1.0/mapSize;\n\ |
| | | const float a = -1.0;\n\ |
| | | const float b = 1.0;\n\ |
| | | float c = -1.0 + offset;\n\ |
| | | float d = 1.0 - offset;\n\ |
| | | float bminusa = b - a;\n\ |
| | | uv.x = (uv.x - a)/bminusa * d - (uv.x - b)/bminusa * c;\n\ |
| | | uv.y = (uv.y - a)/bminusa * d - (uv.y - b)/bminusa * c;\n\ |
| | | if (faceIndex==0) {\n\ |
| | | sampleDirection = vec3(1.0, -uv.y, -uv.x);\n\ |
| | | } else if (faceIndex==1) {\n\ |
| | | sampleDirection = vec3(-1.0, -uv.y, uv.x);\n\ |
| | | } else if (faceIndex==2) {\n\ |
| | | sampleDirection = vec3(uv.x, 1.0, uv.y);\n\ |
| | | } else if (faceIndex==3) {\n\ |
| | | sampleDirection = vec3(uv.x, -1.0, -uv.y);\n\ |
| | | } else if (faceIndex==4) {\n\ |
| | | sampleDirection = vec3(uv.x, -uv.y, 1.0);\n\ |
| | | } else {\n\ |
| | | sampleDirection = vec3(-uv.x, -uv.y, -1.0);\n\ |
| | | }\n\ |
| | | vec3 correctedDirection = vec3( tFlip * sampleDirection.x, sampleDirection.yz );\n\ |
| | | mat3 vecSpace = matrixFromVector( normalize( correctedDirection ) );\n\ |
| | | vec3 rgbColor = vec3(0.0);\n\ |
| | | const int NumSamples = SAMPLES_PER_LEVEL;\n\ |
| | | vec3 vect;\n\ |
| | | float weight = 0.0;\n\ |
| | | for( int i = 0; i < NumSamples; i ++ ) {\n\ |
| | | float sini = sin(float(i));\n\ |
| | | float cosi = cos(float(i));\n\ |
| | | float r = rand(vec2(sini, cosi));\n\ |
| | | vect = ImportanceSampleGGX(vec2(float(i) / float(NumSamples), r), vecSpace, roughness);\n\ |
| | | float dotProd = dot(vect, normalize(sampleDirection));\n\ |
| | | weight += dotProd;\n\ |
| | | vec3 color = envMapTexelToLinear(textureCube(envMap, vect)).rgb;\n\ |
| | | rgbColor.rgb += color;\n\ |
| | | }\n\ |
| | | rgbColor /= float(NumSamples);\n\ |
| | | //rgbColor = testColorMap( roughness ).rgb;\n\ |
| | | gl_FragColor = linearToOutputTexel( vec4( rgbColor, 1.0 ) );\n\ |
| | | }", |
| | | |
| | | blending: NoBlending |
| | | |
| | | } ); |
| | | |
| | | shaderMaterial.type = 'PMREMGenerator'; |
| | | |
| | | return shaderMaterial; |
| | | |
| | | } |
| | | |
| | | return PMREMGenerator; |
| | | |
| | | } )(); |
| | | |
| | | return { PMREMGenerator }; |
| | | } |
| New file |
| | |
| | | /** |
| | | * @author sunag / http://www.sunag.com.br |
| | | */ |
| | | export default function (THREE) { |
| | | let { |
| | | AnimationClip, |
| | | AnimationMixer, |
| | | Euler, |
| | | Matrix4, |
| | | Quaternion, |
| | | QuaternionKeyframeTrack, |
| | | SkeletonHelper, |
| | | Vector2, |
| | | Vector3, |
| | | VectorKeyframeTrack |
| | | } = THREE; |
| | | |
| | | var SkeletonUtils = { |
| | | |
| | | retarget: function () { |
| | | |
| | | var pos = new Vector3(), |
| | | quat = new Quaternion(), |
| | | scale = new Vector3(), |
| | | bindBoneMatrix = new Matrix4(), |
| | | relativeMatrix = new Matrix4(), |
| | | globalMatrix = new Matrix4(); |
| | | |
| | | return function ( target, source, options ) { |
| | | |
| | | options = options || {}; |
| | | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true; |
| | | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true; |
| | | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false; |
| | | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; |
| | | options.hip = options.hip !== undefined ? options.hip : "hip"; |
| | | options.names = options.names || {}; |
| | | |
| | | var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), |
| | | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), |
| | | bindBones, |
| | | bone, name, boneTo, |
| | | bonesPosition, i; |
| | | |
| | | // reset bones |
| | | |
| | | if ( target.isObject3D ) { |
| | | |
| | | target.skeleton.pose(); |
| | | |
| | | } else { |
| | | |
| | | options.useTargetMatrix = true; |
| | | options.preserveMatrix = false; |
| | | |
| | | } |
| | | |
| | | if ( options.preservePosition ) { |
| | | |
| | | bonesPosition = []; |
| | | |
| | | for ( i = 0; i < bones.length; i ++ ) { |
| | | |
| | | bonesPosition.push( bones[ i ].position.clone() ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if ( options.preserveMatrix ) { |
| | | |
| | | // reset matrix |
| | | |
| | | target.updateMatrixWorld(); |
| | | |
| | | target.matrixWorld.identity(); |
| | | |
| | | // reset children matrix |
| | | |
| | | for ( i = 0; i < target.children.length; ++ i ) { |
| | | |
| | | target.children[ i ].updateMatrixWorld( true ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if ( options.offsets ) { |
| | | |
| | | bindBones = []; |
| | | |
| | | for ( i = 0; i < bones.length; ++ i ) { |
| | | |
| | | bone = bones[ i ]; |
| | | name = options.names[ bone.name ] || bone.name; |
| | | |
| | | if ( options.offsets && options.offsets[ name ] ) { |
| | | |
| | | bone.matrix.multiply( options.offsets[ name ] ); |
| | | |
| | | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); |
| | | |
| | | bone.updateMatrixWorld(); |
| | | |
| | | } |
| | | |
| | | bindBones.push( bone.matrixWorld.clone() ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | for ( i = 0; i < bones.length; ++ i ) { |
| | | |
| | | bone = bones[ i ]; |
| | | name = options.names[ bone.name ] || bone.name; |
| | | |
| | | boneTo = this.getBoneByName( name, sourceBones ); |
| | | |
| | | globalMatrix.copy( bone.matrixWorld ); |
| | | |
| | | if ( boneTo ) { |
| | | |
| | | boneTo.updateMatrixWorld(); |
| | | |
| | | if ( options.useTargetMatrix ) { |
| | | |
| | | relativeMatrix.copy( boneTo.matrixWorld ); |
| | | |
| | | } else { |
| | | |
| | | relativeMatrix.getInverse( target.matrixWorld ); |
| | | relativeMatrix.multiply( boneTo.matrixWorld ); |
| | | |
| | | } |
| | | |
| | | // ignore scale to extract rotation |
| | | |
| | | scale.setFromMatrixScale( relativeMatrix ); |
| | | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) ); |
| | | |
| | | // apply to global matrix |
| | | |
| | | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) ); |
| | | |
| | | if ( target.isObject3D ) { |
| | | |
| | | var boneIndex = bones.indexOf( bone ), |
| | | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] ); |
| | | |
| | | globalMatrix.multiply( wBindMatrix ); |
| | | |
| | | } |
| | | |
| | | globalMatrix.copyPosition( relativeMatrix ); |
| | | |
| | | } |
| | | |
| | | if ( bone.parent && bone.parent.isBone ) { |
| | | |
| | | bone.matrix.getInverse( bone.parent.matrixWorld ); |
| | | bone.matrix.multiply( globalMatrix ); |
| | | |
| | | } else { |
| | | |
| | | bone.matrix.copy( globalMatrix ); |
| | | |
| | | } |
| | | |
| | | if ( options.preserveHipPosition && name === options.hip ) { |
| | | |
| | | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) ); |
| | | |
| | | } |
| | | |
| | | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); |
| | | |
| | | bone.updateMatrixWorld(); |
| | | |
| | | } |
| | | |
| | | if ( options.preservePosition ) { |
| | | |
| | | for ( i = 0; i < bones.length; ++ i ) { |
| | | |
| | | bone = bones[ i ]; |
| | | name = options.names[ bone.name ] || bone.name; |
| | | |
| | | if ( name !== options.hip ) { |
| | | |
| | | bone.position.copy( bonesPosition[ i ] ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | if ( options.preserveMatrix ) { |
| | | |
| | | // restore matrix |
| | | |
| | | target.updateMatrixWorld( true ); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | }(), |
| | | |
| | | retargetClip: function ( target, source, clip, options ) { |
| | | |
| | | options = options || {}; |
| | | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; |
| | | options.fps = options.fps !== undefined ? options.fps : 30; |
| | | options.names = options.names || []; |
| | | |
| | | if ( ! source.isObject3D ) { |
| | | |
| | | source = this.getHelperFromSkeleton( source ); |
| | | |
| | | } |
| | | |
| | | var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ), |
| | | delta = 1 / options.fps, |
| | | convertedTracks = [], |
| | | mixer = new AnimationMixer( source ), |
| | | bones = this.getBones( target.skeleton ), |
| | | boneDatas = [], |
| | | positionOffset, |
| | | bone, boneTo, boneData, |
| | | name, i, j; |
| | | |
| | | mixer.clipAction( clip ).play(); |
| | | mixer.update( 0 ); |
| | | |
| | | source.updateMatrixWorld(); |
| | | |
| | | for ( i = 0; i < numFrames; ++ i ) { |
| | | |
| | | var time = i * delta; |
| | | |
| | | this.retarget( target, source, options ); |
| | | |
| | | for ( j = 0; j < bones.length; ++ j ) { |
| | | |
| | | name = options.names[ bones[ j ].name ] || bones[ j ].name; |
| | | |
| | | boneTo = this.getBoneByName( name, source.skeleton ); |
| | | |
| | | if ( boneTo ) { |
| | | |
| | | bone = bones[ j ]; |
| | | boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone }; |
| | | |
| | | if ( options.hip === name ) { |
| | | |
| | | if ( ! boneData.pos ) { |
| | | |
| | | boneData.pos = { |
| | | times: new Float32Array( numFrames ), |
| | | values: new Float32Array( numFrames * 3 ) |
| | | }; |
| | | |
| | | } |
| | | |
| | | if ( options.useFirstFramePosition ) { |
| | | |
| | | if ( i === 0 ) { |
| | | |
| | | positionOffset = bone.position.clone(); |
| | | |
| | | } |
| | | |
| | | bone.position.sub( positionOffset ); |
| | | |
| | | } |
| | | |
| | | boneData.pos.times[ i ] = time; |
| | | |
| | | bone.position.toArray( boneData.pos.values, i * 3 ); |
| | | |
| | | } |
| | | |
| | | if ( ! boneData.quat ) { |
| | | |
| | | boneData.quat = { |
| | | times: new Float32Array( numFrames ), |
| | | values: new Float32Array( numFrames * 4 ) |
| | | }; |
| | | |
| | | } |
| | | |
| | | boneData.quat.times[ i ] = time; |
| | | |
| | | bone.quaternion.toArray( boneData.quat.values, i * 4 ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | mixer.update( delta ); |
| | | |
| | | source.updateMatrixWorld(); |
| | | |
| | | } |
| | | |
| | | for ( i = 0; i < boneDatas.length; ++ i ) { |
| | | |
| | | boneData = boneDatas[ i ]; |
| | | |
| | | if ( boneData ) { |
| | | |
| | | if ( boneData.pos ) { |
| | | |
| | | convertedTracks.push( new VectorKeyframeTrack( |
| | | ".bones[" + boneData.bone.name + "].position", |
| | | boneData.pos.times, |
| | | boneData.pos.values |
| | | ) ); |
| | | |
| | | } |
| | | |
| | | convertedTracks.push( new QuaternionKeyframeTrack( |
| | | ".bones[" + boneData.bone.name + "].quaternion", |
| | | boneData.quat.times, |
| | | boneData.quat.values |
| | | ) ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | mixer.uncacheAction( clip ); |
| | | |
| | | return new AnimationClip( clip.name, - 1, convertedTracks ); |
| | | |
| | | }, |
| | | |
| | | getHelperFromSkeleton: function ( skeleton ) { |
| | | |
| | | var source = new SkeletonHelper( skeleton.bones[ 0 ] ); |
| | | source.skeleton = skeleton; |
| | | |
| | | return source; |
| | | |
| | | }, |
| | | |
| | | getSkeletonOffsets: function () { |
| | | |
| | | var targetParentPos = new Vector3(), |
| | | targetPos = new Vector3(), |
| | | sourceParentPos = new Vector3(), |
| | | sourcePos = new Vector3(), |
| | | targetDir = new Vector2(), |
| | | sourceDir = new Vector2(); |
| | | |
| | | return function ( target, source, options ) { |
| | | |
| | | options = options || {}; |
| | | options.hip = options.hip !== undefined ? options.hip : "hip"; |
| | | options.names = options.names || {}; |
| | | |
| | | if ( ! source.isObject3D ) { |
| | | |
| | | source = this.getHelperFromSkeleton( source ); |
| | | |
| | | } |
| | | |
| | | var nameKeys = Object.keys( options.names ), |
| | | nameValues = Object.values( options.names ), |
| | | sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), |
| | | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), |
| | | offsets = [], |
| | | bone, boneTo, |
| | | name, i; |
| | | |
| | | target.skeleton.pose(); |
| | | |
| | | for ( i = 0; i < bones.length; ++ i ) { |
| | | |
| | | bone = bones[ i ]; |
| | | name = options.names[ bone.name ] || bone.name; |
| | | |
| | | boneTo = this.getBoneByName( name, sourceBones ); |
| | | |
| | | if ( boneTo && name !== options.hip ) { |
| | | |
| | | var boneParent = this.getNearestBone( bone.parent, nameKeys ), |
| | | boneToParent = this.getNearestBone( boneTo.parent, nameValues ); |
| | | |
| | | boneParent.updateMatrixWorld(); |
| | | boneToParent.updateMatrixWorld(); |
| | | |
| | | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld ); |
| | | targetPos.setFromMatrixPosition( bone.matrixWorld ); |
| | | |
| | | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld ); |
| | | sourcePos.setFromMatrixPosition( boneTo.matrixWorld ); |
| | | |
| | | targetDir.subVectors( |
| | | new Vector2( targetPos.x, targetPos.y ), |
| | | new Vector2( targetParentPos.x, targetParentPos.y ) |
| | | ).normalize(); |
| | | |
| | | sourceDir.subVectors( |
| | | new Vector2( sourcePos.x, sourcePos.y ), |
| | | new Vector2( sourceParentPos.x, sourceParentPos.y ) |
| | | ).normalize(); |
| | | |
| | | var laterialAngle = targetDir.angle() - sourceDir.angle(); |
| | | |
| | | var offset = new Matrix4().makeRotationFromEuler( |
| | | new Euler( |
| | | 0, |
| | | 0, |
| | | laterialAngle |
| | | ) |
| | | ); |
| | | |
| | | bone.matrix.multiply( offset ); |
| | | |
| | | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); |
| | | |
| | | bone.updateMatrixWorld(); |
| | | |
| | | offsets[ name ] = offset; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return offsets; |
| | | |
| | | }; |
| | | |
| | | }(), |
| | | |
| | | renameBones: function ( skeleton, names ) { |
| | | |
| | | var bones = this.getBones( skeleton ); |
| | | |
| | | for ( var i = 0; i < bones.length; ++ i ) { |
| | | |
| | | var bone = bones[ i ]; |
| | | |
| | | if ( names[ bone.name ] ) { |
| | | |
| | | bone.name = names[ bone.name ]; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return this; |
| | | |
| | | }, |
| | | |
| | | getBones: function ( skeleton ) { |
| | | |
| | | return Array.isArray( skeleton ) ? skeleton : skeleton.bones; |
| | | |
| | | }, |
| | | |
| | | getBoneByName: function ( name, skeleton ) { |
| | | |
| | | for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) { |
| | | |
| | | if ( name === bones[ i ].name ) |
| | | |
| | | return bones[ i ]; |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | getNearestBone: function ( bone, names ) { |
| | | |
| | | while ( bone.isBone ) { |
| | | |
| | | if ( names.indexOf( bone.name ) !== - 1 ) { |
| | | |
| | | return bone; |
| | | |
| | | } |
| | | |
| | | bone = bone.parent; |
| | | |
| | | } |
| | | |
| | | }, |
| | | |
| | | findBoneTrackData: function ( name, tracks ) { |
| | | |
| | | var regexp = /\[(.*)\]\.(.*)/, |
| | | result = { name: name }; |
| | | |
| | | for ( var i = 0; i < tracks.length; ++ i ) { |
| | | |
| | | // 1 is track name |
| | | // 2 is track type |
| | | var trackData = regexp.exec( tracks[ i ].name ); |
| | | |
| | | if ( trackData && name === trackData[ 1 ] ) { |
| | | |
| | | result[ trackData[ 2 ] ] = i; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return result; |
| | | |
| | | }, |
| | | |
| | | getEqualsBonesNames: function ( skeleton, targetSkeleton ) { |
| | | |
| | | var sourceBones = this.getBones( skeleton ), |
| | | targetBones = this.getBones( targetSkeleton ), |
| | | bones = []; |
| | | |
| | | search : for ( var i = 0; i < sourceBones.length; i ++ ) { |
| | | |
| | | var boneName = sourceBones[ i ].name; |
| | | |
| | | for ( var j = 0; j < targetBones.length; j ++ ) { |
| | | |
| | | if ( boneName === targetBones[ j ].name ) { |
| | | |
| | | bones.push( boneName ); |
| | | |
| | | continue search; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return bones; |
| | | |
| | | }, |
| | | |
| | | clone: function ( source ) { |
| | | |
| | | var sourceLookup = new Map(); |
| | | var cloneLookup = new Map(); |
| | | |
| | | var clone = source.clone(); |
| | | |
| | | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) { |
| | | |
| | | sourceLookup.set( clonedNode, sourceNode ); |
| | | cloneLookup.set( sourceNode, clonedNode ); |
| | | |
| | | } ); |
| | | |
| | | clone.traverse( function ( node ) { |
| | | |
| | | if ( ! node.isSkinnedMesh ) return; |
| | | |
| | | var clonedMesh = node; |
| | | var sourceMesh = sourceLookup.get( node ); |
| | | var sourceBones = sourceMesh.skeleton.bones; |
| | | |
| | | clonedMesh.skeleton = sourceMesh.skeleton.clone(); |
| | | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix ); |
| | | |
| | | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) { |
| | | |
| | | return cloneLookup.get( bone ); |
| | | |
| | | } ); |
| | | |
| | | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix ); |
| | | |
| | | } ); |
| | | |
| | | return clone; |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | |
| | | function parallelTraverse( a, b, callback ) { |
| | | |
| | | callback( a, b ); |
| | | |
| | | for ( var i = 0; i < a.children.length; i ++ ) { |
| | | |
| | | parallelTraverse( a.children[ i ], b.children[ i ], callback ); |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | return { SkeletonUtils }; |
| | | } |
| New file |
| | |
| | | // pages/ThreeModel/ThreeModel.js |
| | | import getMTLLoader from '../../jsm/loaders/MTLLoader.js'; |
| | | import getOBJLoader from '../../jsm/loaders/OBJLoader.js'; |
| | | // import CameraControls from '../../jsm/controls/CameraControls' |
| | | import { |
| | | OrbitControls |
| | | } from '../../jsm/controls/OrbitControls'; |
| | | import * as THREE from '../../libs/three.weapp.js' |
| | | var { |
| | | MTLLoader |
| | | } = getMTLLoader(THREE); |
| | | var OBJLoader = getOBJLoader(THREE); |
| | | var window = THREE.global; |
| | | var camera, scene, requestAnimationFrame, renderer, controls, jeepCar, cameracontrols, texture; |
| | | let t0 = new Date() |
| | | // var animateion = wx.createAnimation({ |
| | | // delay: 0, |
| | | // duration: 800, |
| | | // timingFunction: "ease" |
| | | // }) |
| | | Page({ |
| | | |
| | | /** |
| | | * 页面的初始数据 |
| | | */ |
| | | data: { |
| | | isOnAccelerometerChange: true, |
| | | threeLeft: "0", |
| | | vdisplay: "none", |
| | | view: "1", |
| | | change: false, |
| | | back: true, |
| | | thirdView: false |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * 生命周期函数--监听页面加载 |
| | | */ |
| | | onLoad: function (options) { |
| | | this.start() |
| | | }, |
| | | |
| | | /** |
| | | * 生命周期函数--监听页面初次渲染完成 |
| | | */ |
| | | onReady: function () { |
| | | |
| | | }, |
| | | onUnload: function () { |
| | | |
| | | }, |
| | | start: function () { |
| | | let that = this; |
| | | wx.createSelectorQuery() |
| | | .select('#c') |
| | | .node() |
| | | .exec((res) => { |
| | | console.log(res) |
| | | const canvas = new THREE.global.registerCanvas(res[0].node) |
| | | console.log(canvas, "canvas") |
| | | if (canvas != undefined) { |
| | | canvas.width = canvas._width; |
| | | canvas.height = canvas._height; |
| | | requestAnimationFrame = canvas.requestAnimationFrame; |
| | | that.init(canvas); |
| | | } |
| | | |
| | | }) |
| | | }, |
| | | onProgress: function (xhr) { |
| | | if (xhr.lengthComputable) { |
| | | |
| | | let percentComplete = xhr.loaded / xhr.total * 100; |
| | | // console.log('model ' + Math.round(percentComplete, 2) + '% downloaded'); |
| | | |
| | | } |
| | | }, |
| | | |
| | | onError: function () {}, |
| | | // 模型加载 |
| | | init: function (canvas) { |
| | | let that = this; |
| | | // render |
| | | renderer = new THREE.WebGLRenderer({ |
| | | canvas, |
| | | antialias: true, |
| | | alpha: true, |
| | | preserveDrawingBuffer:true |
| | | }); |
| | | |
| | | renderer.setPixelRatio(window.devicePixelRatio); |
| | | renderer.setSize(window.innerHeight, window.innerHeight); |
| | | |
| | | |
| | | // camera |
| | | |
| | | console.log(that.data.view, "函数内") |
| | | if (that.data.view == "1") { |
| | | camera = new THREE.PerspectiveCamera(10, 2, 1, -1, 100, 1000); |
| | | camera.position.z = 500; |
| | | camera.position.y = 300; |
| | | camera.lookAt({ |
| | | x: 0, |
| | | y: 200, |
| | | z: 0 |
| | | }) |
| | | } else if (that.data.view == "2") { |
| | | // camera.position.set(400,400,500) |
| | | camera = new THREE.PerspectiveCamera(60, 2, 1, -1, 1, 10); |
| | | camera.position.y = 20; |
| | | camera.position.x = -10; |
| | | camera.position.z = -10; |
| | | camera.lookAt({ |
| | | x: 0, |
| | | y: 50, |
| | | z: 0 |
| | | }) |
| | | } |
| | | // else if (that.data.view == "3") { |
| | | // camera = new THREE.PerspectiveCamera(45, 1, 1, 500); |
| | | // camera.position.set = (400,400,500); |
| | | // camera.lookAt({ |
| | | // x:0, |
| | | // y:400, |
| | | // z:0 |
| | | // }) |
| | | |
| | | // } |
| | | // controls |
| | | controls = new OrbitControls(camera, canvas); |
| | | controls.minZoom = 0.5; |
| | | controls.target.set(1, 5, 1); |
| | | |
| | | |
| | | // scan和light |
| | | scene = new THREE.Scene(); |
| | | let ambientLight = new THREE.AmbientLight(0x444444, 0.4); |
| | | scene.add(ambientLight); |
| | | let AmbientLight = new THREE.AmbientLight(0xffffff, 1) |
| | | // let pointLight = new THREE.PointLight(0xffffff, 3); |
| | | camera.add(AmbientLight); |
| | | scene.add(camera); |
| | | |
| | | // 加载obj模型 |
| | | let loader = new OBJLoader(); |
| | | loader.load('http://47.92.33.19:9001/version/OBJ/Diaobao.obj', function (obj) { |
| | | console.log(obj) |
| | | }); |
| | | // 加载mtl纹理 |
| | | let mtlLoader = new MTLLoader(); |
| | | mtlLoader.load( |
| | | 'http://47.92.33.19:9001/version/OBJ/Diaobao.mtl', |
| | | function (materials) { |
| | | materials.preload() |
| | | // console.log(that, "this") |
| | | loader.setMaterials(materials) |
| | | // 加载obj |
| | | loader.load('http://47.92.33.19:9001/version/OBJ/Diaobao.obj', function (obj) { |
| | | // 模型文件太大,缩小一下比例,方便显示 |
| | | obj.scale.set(0.5, 0.5, 0.5) |
| | | obj.position.y = -15 |
| | | // 设置可以投影 |
| | | obj.children.forEach(item => { |
| | | item.castShadow = true |
| | | item.receiveShadow = true |
| | | // console.log(item,"item") |
| | | }) |
| | | jeepCar = obj |
| | | // 添加到场景 |
| | | scene.add(jeepCar) |
| | | }) |
| | | } |
| | | ) |
| | | that.animate() |
| | | }, |
| | | |
| | | animate: function () { |
| | | let that = this; |
| | | // controls.update(); |
| | | // let t1 = new Date(); //本次时间 |
| | | // let t = t1 - t0; // 时间差 |
| | | // camera.translateX(0.001 * t/3) //沿着Y轴旋转着从大到小 |
| | | // camera.translateZ(0.001 * t/3) //沿着Z轴由近到远 |
| | | // camera.rotateY(0.0001 * t/3); //物体的均匀从左到又平移可以用相机旋转Y轴来实现 |
| | | camera.lookAt(scene.position); |
| | | requestAnimationFrame(that.animate); |
| | | renderer.render(scene, camera); |
| | | }, |
| | | |
| | | // 模型拖动事件 |
| | | touchStart(e) { |
| | | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) |
| | | }, |
| | | touchMove(e) { |
| | | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) |
| | | }, |
| | | touchEnd(e) { |
| | | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) |
| | | }, |
| | | |
| | | // 页面内容事件 |
| | | // 切换碉堡内部和外部按键 |
| | | FirstView: function () { |
| | | |
| | | this.setData({ |
| | | change: !this.data.change |
| | | }) |
| | | let that = this |
| | | if (that.data.change) { |
| | | this.setData({ |
| | | view: "2", |
| | | thirdView: true |
| | | }) |
| | | } else { |
| | | this.setData({ |
| | | view: "1", |
| | | thirdView: false |
| | | }) |
| | | } |
| | | that.start() |
| | | }, |
| | | // 切换碉堡内部的视角 |
| | | // TView: function () { |
| | | // if (this.data.changeTop) { |
| | | // this.setData({ |
| | | // view: "3", |
| | | // changeTop: !this.data.changeTop |
| | | // }) |
| | | // } else { |
| | | // this.setData({ |
| | | // view: "2", |
| | | // changeTop: !this.data.changeTop |
| | | // }) |
| | | // } |
| | | // this.start() |
| | | // }, |
| | | // 播放视频 |
| | | BindShow: function () { |
| | | console.log("点击了") |
| | | this.setData({ |
| | | threeLeft: "100%", |
| | | vdisplay: "block", |
| | | back: false |
| | | }) |
| | | }, |
| | | // 回退上一页 |
| | | backIndex: function () { |
| | | wx.navigateBack({ |
| | | delta: 0, |
| | | }) |
| | | }, |
| | | // 视频后退按钮 |
| | | cloth: function () { |
| | | console.log("点击了") |
| | | this.setData({ |
| | | threeLeft: "0%", |
| | | vdisplay: "none", |
| | | back: true |
| | | }) |
| | | }, |
| | | }) |
| New file |
| | |
| | | { |
| | | "navigationStyle":"custom", |
| | | "pageOrientation": "landscape", |
| | | "usingComponents": {} |
| | | } |
| New file |
| | |
| | | |
| | | <view style="width: 100%;height: 100%;overflow: hidden;"> |
| | | <!--pages/hands/hands.wxml--> |
| | | <camera frame-size="medium" |
| | | mode="normal" |
| | | device-position="back" |
| | | resolution="high" |
| | | flash="off"/> |
| | | <!-- 操作按钮模型角度切换 --> |
| | | <view class="button" bindtap="FirstView"> |
| | | <text class="iconfont icon-view firstView" id="change" bindanimationend="{{ani}}" style="position: absolute; left: 50rpx; top: 49rpx; width: 50rpx; height: 50rpx; display: block; box-sizing: border-box"></text> |
| | | <text >视角切换</text> |
| | | </view> |
| | | |
| | | <view class="thirdView" wx:if="{{thirdView}}" bindtap="TView"> |
| | | 查看简介 |
| | | </view> |
| | | |
| | | <!-- 点击截图浏览视频 --> |
| | | <view class="playVideo"> |
| | | <view class="mask"> |
| | | <text bindtap="BindShow" class="iconfont icon-bofang"></text> |
| | | </view> |
| | | <image src="http://47.92.33.19:9001/version/OBJ/DB_dm.jpg" ></image> |
| | | </view> |
| | | |
| | | <view style="display: {{vdisplay}};width: 100%;height: 100%;position: absolute;top:0;z-index: 350;left: 0;"> |
| | | <text id="cloth" class="iconfont icon-guanbi" |
| | | bindtap="cloth"></text> |
| | | <video autoplay="true" |
| | | show-mute-btn="true" |
| | | bindended="cloth" |
| | | vslide-gesture-in-fullscreen="true" |
| | | src="https://o.666visa.cn/video/home.mp4"></video> |
| | | </view> |
| | | |
| | | <!-- 模型canvas组件 --> |
| | | <canvas style="left: {{threeLeft}};" type="webgl" id="c" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" bindtouchcancel="touchCancel" bindlongtap="longTap" bindtap="tap"></canvas> |
| | | <icon class="iconfont icon-guanbi" wx:if="{{back}}" bindtap="backIndex"></icon> |
| | | <!-- <text class="iconfont icon-guanbi" wx:if="{{!display}}"></text> --> |
| | | </view> |
| New file |
| | |
| | | /* pages/ThreeModel/ThreeModel.wxss */ |
| | | /* pages/hands/hands.wxss */ |
| | | camera { |
| | | position: absolute; |
| | | width: 100vw; |
| | | height: 100vh; |
| | | z-index: 10; |
| | | } |
| | | |
| | | video{ |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: 380; |
| | | } |
| | | #c { |
| | | width: 100%; |
| | | height: 100%; |
| | | position: absolute; |
| | | z-index: 100; |
| | | } |
| | | |
| | | text{ |
| | | display: block; |
| | | width: 50rpx; |
| | | font-size: 12rpx; |
| | | color: white; |
| | | text-align: center; |
| | | position: absolute; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | bottom: 10rpx; |
| | | z-index: 300; |
| | | } |
| | | .playVideo{ |
| | | position: absolute; |
| | | z-index: 300; |
| | | bottom: 0; |
| | | left: 0; |
| | | width: 200rpx; |
| | | height: 150rpx; |
| | | } |
| | | image{ |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 200rpx; |
| | | height: 150rpx; |
| | | } |
| | | .mask{ |
| | | z-index: 300; |
| | | width: 200rpx; |
| | | height: 150rpx; |
| | | position: relative; |
| | | background-color: rgba(0, 0, 0, 0.8); |
| | | } |
| | | .icon-bofang{ |
| | | position: absolute; |
| | | font-size: 50rpx; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%,-50%); |
| | | margin: auto; |
| | | } |
| | | .button:active{ |
| | | background-color: teal; |
| | | /* transform: rotateZ(180deg); */ |
| | | } |
| | | #change{ |
| | | display: block; |
| | | position: absolute; |
| | | transition-duration: 0.1s; |
| | | transform-origin: 0 0; |
| | | width: 50rpx; |
| | | height: 50rpx; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%,-60%); |
| | | font-size: 50rpx; |
| | | color: white; |
| | | } |
| | | .video{ |
| | | left: 10px; |
| | | top: 100px; |
| | | } |
| | | .button{ |
| | | transition-duration: 1s; |
| | | position: absolute; |
| | | right: 20px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | height: 100rpx; |
| | | width: 100rpx; |
| | | background-color: rgba(0, 0, 0, 0.3); |
| | | z-index: 300; |
| | | border-radius: 50%; |
| | | } |
| | | .firstView{ |
| | | right: 10px; |
| | | top: 100px; |
| | | } |
| | | .secondView{ |
| | | right: 10px; |
| | | top: 150px; |
| | | } |
| | | #cloth{ |
| | | z-index: 400; |
| | | } |
| | | .icon-guanbi{ |
| | | color: aliceblue; |
| | | z-index: 350; |
| | | position: absolute; |
| | | top: 20rpx; |
| | | left: 20rpx; |
| | | display: block; |
| | | width: 50rpx; |
| | | height: 50rpx; |
| | | font-size: 20rpx; |
| | | } |
| | | |
| | | .thirdView{ |
| | | position: absolute; |
| | | width: 70rpx; |
| | | height: 70rpx; |
| | | line-height: 70rpx; |
| | | text-align: center; |
| | | border-radius: 50%; |
| | | background-color: rgba(0, 0, 0, 0.4); |
| | | bottom: 20rpx; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | font-size: 12rpx; |
| | | transition-duration: 1s; |
| | | color: white; |
| | | z-index: 300; |
| | | } |
| | | .thirdView:active{ |
| | | background-color: teal; |
| | | } |
| | | |
| New file |
| | |
| | | export class CrsClient { |
| | | /** |
| | | * @param config { {cloudKey: string, token: string, clientHost: string, jpegQuality: number} } |
| | | * @param canvas { HTMLCanvasElement } |
| | | */ |
| | | constructor(config, canvas) { |
| | | this.config = config; |
| | | this.canvas = canvas; |
| | | this.context = canvas.getContext('2d'); |
| | | } |
| | | |
| | | /** |
| | | * 发起一次CRS请求 |
| | | * @param frame { {data: ArrayBuffer, height: number, width: number} } 相机帧 |
| | | * @return {Promise} |
| | | */ |
| | | queryImage(frame) { |
| | | /* |
| | | * 从相机帧开始,发起一次CRS请求的步骤: |
| | | * 1. 将相机帧画到canvas上 |
| | | * 2. 调用canvas.toDataURL得到JPEG图片的base64 (需要小程序基础库2.12.0及以上) |
| | | * 3. 填充请求参数 |
| | | * 4. 发送CRS请求并返回 |
| | | */ |
| | | let ctxImageData = this.context.createImageData(frame.width, frame.height); //#1 |
| | | ctxImageData.data.set(new Uint8ClampedArray(frame.data)); //#1 |
| | | this.context.putImageData(ctxImageData, 0, 0); //#1 |
| | | let dataUrl = this.canvas.toDataURL("image/jpeg", this.config.quality); //#2 |
| | | let base64 = dataUrl.substr(23); //#2 去除dataURL头,留下文件内容 |
| | | |
| | | const params = { //#3 添加cloudKey参数 |
| | | image: base64, |
| | | notracking: "true", |
| | | appId: this.config.cloudKey, |
| | | }; |
| | | |
| | | return new Promise((resolve, reject) => { //#4 发送CRS请求 |
| | | wx.request({ |
| | | url: `${this.config.clientHost}/search/`, |
| | | method: 'post', |
| | | data: params, |
| | | header: { |
| | | 'Authorization': this.config.token, |
| | | 'content-type': 'application/json' |
| | | }, |
| | | success: res => resolve(res.data), |
| | | fail: err => reject(err), |
| | | }); |
| | | }); |
| | | } |
| | | } |
| New file |
| | |
| | | import {CrsClient} from "./crsClient"; |
| | | |
| | | const systemInfo = wx.getSystemInfoSync(); |
| | | |
| | | const SELECT_TYPE = { |
| | | NONE: 0, |
| | | IMAGE: 1, |
| | | VIDEO: 2, |
| | | }; |
| | | |
| | | Page({ |
| | | data: { |
| | | showOverlay: true, |
| | | showSelect: false, |
| | | SELECT_TYPE: SELECT_TYPE, |
| | | selectType: 0, |
| | | |
| | | //CRS配置 |
| | | config: { |
| | | token: "7iSUp2KvzoX/oFQ0yvSRqRBlxGl4lebWl5aSaAhhvrXW3ISHVVivpPNnF2FzPW92Om19vJyG0gX0/lg+lEGXjSKbokh8vdlmW9JYU8i/Zz1cppifNB7masloftEuWU5/onxFnSmXPLuCtj+lbfZ/cZdYB95cJHCl/Z2nX+gLYOaJeYf8ZXjGIPOe26BvC6VMo2rgBEToebDFcyRina2AjLJeRGohoERyTNdysM0ZqKFLClQs15Fam7FbG3bH3MDYff8CtKoa8H3KrGvfP56xMjnx1wYJ8HCVYji6nmzPHR9MypLEZ6jztWg3/LfCTd1s24FVhRiHq+4t7DVH6iYk71sm317wqLVe+G62uWs7pHN+/CqBEEgQU3CAPus4uFiXTe/AAErZVmg5j5dU8lI64eVnBlHpuPPtpGJrneA0FBegBKa1uknuveHcRPToAhm+Jfs4s8oaf06I0PNl4Inwow==", |
| | | cloudKey: "2c5cecc598d1868d829470f0cfbc6352", |
| | | clientHost: 'https://cn1-crs.easyar.com:8443', //服务器一般不变 |
| | | jpegQuality: 0.7, //JPEG压缩质量,建议不低于70% |
| | | minInterval: 1000, //最短的两次CRS请求间隔 |
| | | }, |
| | | //识别到这个数组中的ID就触发内容 |
| | | targetIds: [ |
| | | "2839b04f-04b8-447b-8ce2-9ccb2a290d90", |
| | | "7324cccc-ac18-46c6-b3ef-5532e03d50e8", |
| | | "b5f96a1e-40e1-420a-b27f-b7454b7c878d", |
| | | ], |
| | | |
| | | showLoading: false, |
| | | showLoadingText: "", |
| | | }, |
| | | |
| | | /** @type {CameraFrameListener} 相机帧回调 */ |
| | | listener: undefined, |
| | | /** @type {HTMLCanvasElement} canvas对象 */ |
| | | canvas: undefined, |
| | | |
| | | /** @type {boolean} 是否需要持续识别,在点击“识别体验”之后和识别成功之前为true */ |
| | | runningCrs: undefined, |
| | | /** @type {boolean} 当前是否正在进行CRS请求 */ |
| | | busy: undefined, |
| | | /** @type {CrsClient} 负责发起CRS请求的对象 */ |
| | | crsClient: undefined, |
| | | /** @type {number} 最后一次CRS请求的事件,用于判断是否满足最短请求间隔 */ |
| | | last: undefined, |
| | | |
| | | onLoad: function () { |
| | | }, |
| | | onReady: function () { |
| | | if (systemInfo.platform === "devtools") { //开发工具不会触发initdone事件,于是在onReady手动触发 |
| | | this.onCameraInit(); |
| | | } |
| | | }, |
| | | |
| | | showLoading(text) { |
| | | this.setData({ |
| | | showLoading: true, |
| | | showLoadingText: text, |
| | | }); |
| | | }, |
| | | hideLoading() { |
| | | this.setData({ |
| | | showLoading: false, |
| | | }); |
| | | }, |
| | | |
| | | //图像识别部分: |
| | | |
| | | onShow: function () { |
| | | if (this.listener) this.listener.start(); //页面隐藏时相机帧的监听会自动停止,但恢复展示时不会自动启动,这里手动启动 |
| | | }, |
| | | |
| | | onCameraInit: function () { |
| | | //找到canvas对象 |
| | | const query = wx.createSelectorQuery(); |
| | | query.select('#capture') |
| | | .fields({node: true}) |
| | | .exec((res) => { |
| | | const canvas = res[0].node; |
| | | //设置canvas内部尺寸为480*640,frame-size="medium"的设置下相机帧大多是480*640 |
| | | canvas.width = 480; |
| | | canvas.height = 640; |
| | | this.canvas = canvas; |
| | | |
| | | this.crsClient = new CrsClient(this.data.config, this.canvas); |
| | | |
| | | //开始监听相机帧 |
| | | let cameraContext = wx.createCameraContext(); |
| | | this.listener = cameraContext.onCameraFrame(frame => { |
| | | if (!this.canvas) return; |
| | | let canvas = this.canvas; |
| | | //如果尺寸不匹配,就修改canvas尺寸以适应相机帧 |
| | | if (canvas.width !== frame.width || canvas.height !== frame.height) { |
| | | canvas.width = frame.width; |
| | | canvas.height = frame.height; |
| | | } |
| | | |
| | | this.queryImage(frame); |
| | | }); |
| | | this.listener.start(); |
| | | }); |
| | | }, |
| | | |
| | | queryImage: function (frame) { |
| | | if (!this.runningCrs || this.busy || !this.crsClient) return; |
| | | |
| | | //最短的两次CRS请求间隔 |
| | | let now = new Date().getTime(); |
| | | if (this.last && (now - this.last < this.data.config.minInterval)) return; |
| | | this.last = now; |
| | | |
| | | this.busy = true; //如果正在进行CRS请求,就不允许再次请求 |
| | | |
| | | this.crsClient.queryImage(frame).then(res => { |
| | | console.log(res,"res") |
| | | if (!this.runningCrs) return; //避免在停止后仍然触发 |
| | | let result = res && res.result; |
| | | if (!result) return; |
| | | |
| | | if (result.target) { |
| | | console.log("识别成功", result.target.targetId); |
| | | //如果待触发的id列表中存在识别到的这个id,就触发 |
| | | if (this.data.targetIds.find(targetId => targetId === result.target.targetId)) { |
| | | console.log("4444444444") |
| | | this.onResult(result.target); |
| | | } |
| | | // wx.navigateTo({ |
| | | // url: '/pages/hands/hands', |
| | | // }) |
| | | } else { |
| | | console.log("识别失败", result.message); |
| | | } |
| | | this.busy = false; |
| | | }).catch(e => { |
| | | this.busy = false; |
| | | console.log(e); |
| | | }); //小程序iOS端不支持finally,所以在then和catch里分别设置busy = false |
| | | }, |
| | | |
| | | onResult: function (target) { |
| | | this.runningCrs = false; |
| | | this.hideLoading(); |
| | | console.log("触发内容!",target); |
| | | if (target.meta) { |
| | | console.log("meta base64:", target.meta); |
| | | wx.navigateTo({ |
| | | url: '/pages/ThreeModel/ThreeModel', |
| | | }) |
| | | } |
| | | this.setData({ |
| | | showOverlay: false, |
| | | showContent: true, |
| | | selectType: SELECT_TYPE.IMAGE, |
| | | }); |
| | | }, |
| | | |
| | | //界面: |
| | | |
| | | back: function () { |
| | | this.runningCrs = false; |
| | | this.setData({ |
| | | showOverlay: true, |
| | | showContent: false, |
| | | selectType: SELECT_TYPE.NONE, |
| | | }); |
| | | this.hideLoading(); |
| | | }, |
| | | |
| | | experience: function () { |
| | | this.setData({ |
| | | showOverlay: false, |
| | | showContent: true, |
| | | selectType: SELECT_TYPE.IMAGE, |
| | | }); |
| | | }, |
| | | goThreeD:function(){ |
| | | wx.navigateTo({ |
| | | url: '/pages/ThreeModel/ThreeModel', |
| | | }) |
| | | }, |
| | | scan: function () { |
| | | this.runningCrs = true; |
| | | this.setData({ |
| | | showOverlay: false, |
| | | showContent: false, |
| | | selectType: SELECT_TYPE.NONE, |
| | | }); |
| | | this.showLoading("识别中"); |
| | | }, |
| | | |
| | | download: function () { |
| | | wx.saveImageToPhotosAlbum({ |
| | | filePath: "/images/namecard.jpg", |
| | | success: res => { |
| | | wx.showToast({title: "已保存到相册", icon: "none"}); |
| | | }, |
| | | fail: res => { |
| | | wx.showToast({title: "保存失败", icon: "none"}); |
| | | }, |
| | | }); |
| | | }, |
| | | |
| | | selectContent: function (e) { |
| | | this.setData({ |
| | | selectType: e.currentTarget.dataset.contenttype, |
| | | }); |
| | | }, |
| | | }); |
| New file |
| | |
| | | { |
| | | "usingComponents": { |
| | | "loading": "/components/loading/loading" |
| | | } |
| | | } |
| New file |
| | |
| | | <!-- 相机组件,放在UI最底层显示相机内容 --> |
| | | <camera frame-size="medium" |
| | | bindinitdone="onCameraInit" |
| | | mode="normal" |
| | | device-position="back" |
| | | resolution="high" |
| | | flash="off"/> |
| | | <!-- canvas组件,用于进行图片压缩,位置在屏幕外,不可见 --> |
| | | <canvas type="2d" |
| | | id="capture" |
| | | style="width:1px; height: 1px;"/> |
| | | |
| | | <view id="overlay" wx:if="{{showOverlay}}"> |
| | | <button bindtap="goThreeD">点击我去3D页面</button> |
| | | <view id="distinguish" bindtap="scan"> |
| | | <text |
| | | |
| | | class="iconfont icon-yanzhengmashibie" id="icon"></text> |
| | | <text id="scan" class="primary-button">点击识别</text> |
| | | </view> |
| | | </view> |
| | | <text |
| | | bindtap="back" |
| | | class="iconfont icon-guanbi" wx:if="{{showLoading}}"></text> |
| | | <view id="loading" wx:if="{{showLoading}}"> |
| | | |
| | | <loading text="{{showLoadingText}}"></loading> |
| | | </view> |
| New file |
| | |
| | | camera { |
| | | position: absolute; |
| | | width: 100vw; |
| | | height: 100vh; |
| | | z-index: 10; |
| | | } |
| | | |
| | | #capture { |
| | | position: absolute; |
| | | left: 750rpx; |
| | | } |
| | | |
| | | #overlay { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100vw; |
| | | height: 100vh; |
| | | background: rgba(63, 63, 63, 0.6); |
| | | z-index: 30; |
| | | } |
| | | /* 识别样式 */ |
| | | #loading { |
| | | position: absolute; |
| | | width: fit-content; |
| | | height: fit-content; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | margin: auto; |
| | | z-index: 40; |
| | | } |
| | | .icon-guanbi{ |
| | | display: block; |
| | | width: 70rpx; |
| | | height: 70rpx; |
| | | position: absolute; |
| | | top: 50rpx; |
| | | left: 50rpx; |
| | | font-size: 70rpx; |
| | | color: black; |
| | | z-index: 200; |
| | | } |
| | | #distinguish{ |
| | | width: 400rpx; |
| | | height: 100rpx; |
| | | background-color: turquoise; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | margin: auto; |
| | | border-radius: 15rpx; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | #icon{ |
| | | display: block; |
| | | width: 60rpx; |
| | | height: 60rpx; |
| | | font-size: 60rpx; |
| | | color: white; |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| New file |
| | |
| | | { |
| | | "description": "项目配置文件", |
| | | "packOptions": { |
| | | "ignore": [] |
| | | }, |
| | | "setting": { |
| | | "urlCheck": false, |
| | | "es6": true, |
| | | "enhance": true, |
| | | "postcss": true, |
| | | "preloadBackgroundData": false, |
| | | "minified": true, |
| | | "newFeature": false, |
| | | "coverView": true, |
| | | "nodeModules": false, |
| | | "autoAudits": false, |
| | | "showShadowRootInWxmlPanel": true, |
| | | "scopeDataCheck": false, |
| | | "uglifyFileName": false, |
| | | "checkInvalidKey": true, |
| | | "checkSiteMap": true, |
| | | "uploadWithSourceMap": true, |
| | | "compileHotReLoad": false, |
| | | "lazyloadPlaceholderEnable": false, |
| | | "useMultiFrameRuntime": true, |
| | | "useApiHook": true, |
| | | "useApiHostProcess": true, |
| | | "babelSetting": { |
| | | "ignore": [], |
| | | "disablePlugins": [], |
| | | "outputPath": "" |
| | | }, |
| | | "enableEngineNative": false, |
| | | "useIsolateContext": true, |
| | | "userConfirmedBundleSwitch": false, |
| | | "packNpmManually": false, |
| | | "packNpmRelationList": [], |
| | | "minifyWXSS": true, |
| | | "disableUseStrict": false, |
| | | "minifyWXML": true, |
| | | "showES6CompileOption": false, |
| | | "useCompilerPlugins": false |
| | | }, |
| | | "compileType": "miniprogram", |
| | | "libVersion": "2.20.1", |
| | | "appid": "wx0a4472ff1d1b0ddd", |
| | | "projectname": "DiaobaoFinallyTest", |
| | | "debugOptions": { |
| | | "hidedInDevtools": [] |
| | | }, |
| | | "scripts": {}, |
| | | "staticServerOptions": { |
| | | "baseURL": "", |
| | | "servePath": "" |
| | | }, |
| | | "isGameTourist": false, |
| | | "condition": { |
| | | "search": { |
| | | "list": [] |
| | | }, |
| | | "conversation": { |
| | | "list": [] |
| | | }, |
| | | "game": { |
| | | "list": [] |
| | | }, |
| | | "plugin": { |
| | | "list": [] |
| | | }, |
| | | "gamePlugin": { |
| | | "list": [] |
| | | }, |
| | | "miniprogram": { |
| | | "list": [] |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", |
| | | "rules": [{ |
| | | "action": "allow", |
| | | "page": "*" |
| | | }] |
| | | } |
| New file |
| | |
| | | const formatTime = date => { |
| | | const year = date.getFullYear() |
| | | const month = date.getMonth() + 1 |
| | | const day = date.getDate() |
| | | const hour = date.getHours() |
| | | const minute = date.getMinutes() |
| | | const second = date.getSeconds() |
| | | |
| | | return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` |
| | | } |
| | | |
| | | const formatNumber = n => { |
| | | n = n.toString() |
| | | return n[1] ? n : `0${n}` |
| | | } |
| | | |
| | | module.exports = { |
| | | formatTime |
| | | } |