├── .gitignore ├── README.md ├── Web.config ├── chest.html ├── chest.js ├── envmaps └── Yokohama │ ├── negx.jpg │ ├── negy.jpg │ ├── negz.jpg │ ├── posx.jpg │ ├── posy.jpg │ ├── posz.jpg │ └── readme.md ├── fire-hydrant.html ├── fire-hydrant.js ├── index.css ├── index.html ├── libs └── three.js │ ├── OBJLoader.js │ └── r82dev │ ├── controls │ └── OrbitControls.js │ ├── three.js │ └── three.min.js ├── models ├── Chest │ ├── Chest.obj │ ├── chest_albedo.png │ ├── chest_ao.png │ ├── chest_metalness.png │ ├── chest_normal.png │ └── chest_roughness.png ├── FireHydrant │ ├── FireHydrantMesh.obj │ ├── fire_hydrant_Base_Color.png │ ├── fire_hydrant_Metallic.png │ ├── fire_hydrant_Mixed_AO.png │ ├── fire_hydrant_Normal_OpenGL.png │ └── fire_hydrant_Roughness.png └── NormalTangentTest │ ├── NormalTangentTestPlatform.mtl │ ├── NormalTangentTestPlatform.obj │ ├── NormalTangentTestPlatform_high.mtl │ ├── NormalTangentTestPlatform_high.obj │ ├── NormalTangentTest_BaseColor.png │ ├── NormalTangentTest_Metallic.png │ ├── NormalTangentTest_Normal.png │ ├── NormalTangentTest_Occlusion.png │ ├── NormalTangentTest_OcclusionRoughnessMetallic.png │ ├── NormalTangentTest_Roughness.png │ └── UV_label_alpha.png ├── normal-tangent-readme.html ├── normal-tangent-test.html ├── normal-tangent-test.js ├── screenshots ├── FireHydrant.png └── NormalTangentTest │ ├── BlenderApplyNormalMap.png │ ├── BlenderNormalMapImportButton.png │ ├── ExampleOfCorrectNormalMap.png │ ├── ExampleOfDirectXNormalsUsedInBlender.png │ ├── ExampleOfUVDirections.png │ ├── ExampleOfWrongBehavior.png │ ├── OffAxis.png │ └── UV_Positions_Arrows.png └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | Desktop.ini 4 | *.suo 5 | *.blend1 6 | node_modules 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Physically Based Rendering (PBR) Tests 2 | 3 | Click the link for more info: 4 | 5 | https://emackey.github.io/testing-pbr/ 6 | -------------------------------------------------------------------------------- /Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /chest.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Treasure Chest 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | README 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chest.js: -------------------------------------------------------------------------------- 1 | init(); 2 | animate(); 3 | 4 | // Thanks to @cx20 for this sample code! 5 | function init() { 6 | width = window.innerWidth; 7 | height = window.innerHeight; 8 | 9 | scene = new THREE.Scene(); 10 | 11 | var ambient = new THREE.AmbientLight( 0x101030 ); 12 | scene.add( ambient ); 13 | 14 | var directionalLight = new THREE.DirectionalLight(0xffeedd); 15 | directionalLight.position.set(0.5, 0.8, 1); 16 | scene.add(directionalLight); 17 | 18 | var fillLight = new THREE.DirectionalLight(0xddeeff); 19 | fillLight.position.set(-0.5, 0.2, -1); 20 | scene.add(fillLight); 21 | 22 | // Creative Commons license, downloaded from http://www.humus.name/index.php?page=Textures 23 | var envPath = "envmaps/Yokohama/"; 24 | var urls = [ 25 | envPath + 'posx.jpg', 26 | envPath + 'negx.jpg', 27 | envPath + 'posy.jpg', 28 | envPath + 'negy.jpg', 29 | envPath + 'posz.jpg', 30 | envPath + 'negz.jpg' 31 | ]; 32 | 33 | var reflectionCube = new THREE.CubeTextureLoader().load(urls); 34 | reflectionCube.format = THREE.RGBFormat; 35 | 36 | // This line displays the reflectionCube as the scene's background. 37 | scene.background = reflectionCube; 38 | 39 | camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 200 ); 40 | camera.position.z = 300; 41 | camera.position.copy(new THREE.Vector3(0, 2, 3)); 42 | 43 | var manager = new THREE.LoadingManager(); 44 | manager.onProgress = function ( item, loaded, total ) { 45 | console.log( item, loaded, total ); 46 | }; 47 | 48 | var objLoader = new THREE.OBJLoader(manager); 49 | objLoader.load('models/Chest/Chest.obj', function(object) { 50 | var mainMesh; 51 | object.traverse(function (child) { 52 | if (child instanceof THREE.Mesh) { 53 | mainMesh = child; 54 | } 55 | }); 56 | 57 | object.position.set(0, 0, 0); 58 | object.scale.set(4, 4, 4); 59 | 60 | var textureLoader = new THREE.TextureLoader(); 61 | mainMesh.material = new THREE.MeshStandardMaterial({ 62 | map: textureLoader.load('models/Chest/chest_albedo.png'), 63 | normalMap: textureLoader.load('models/Chest/chest_normal.png'), 64 | roughnessMap: textureLoader.load('models/Chest/chest_roughness.png'), 65 | metalnessMap: textureLoader.load('models/Chest/chest_metalness.png'), 66 | aoMap: textureLoader.load('models/Chest/chest_ao.png'), 67 | 68 | //roughness: 0, 69 | //metalness: 1, 70 | aoMapIntensity: 0.5, // The ao map appears to quash the reflectionCube. 71 | envMap: reflectionCube 72 | }); 73 | 74 | scene.add(object); 75 | window.mainObject = object; // for debugging only 76 | window.mainMesh = mainMesh; // for debugging only 77 | }); 78 | 79 | renderer = new THREE.WebGLRenderer(); 80 | renderer.setClearColor( 0xaaaaaa ); 81 | 82 | controls = new THREE.OrbitControls( camera, renderer.domElement ); 83 | controls.userPan = false; 84 | controls.userPanSpeed = 0.0; 85 | controls.maxDistance = 5000.0; 86 | controls.maxPolarAngle = Math.PI * 0.895; 87 | controls.autoRotate = false; 88 | controls.autoRotateSpeed = -10.0; 89 | 90 | renderer.setSize( width, height ); 91 | document.body.appendChild( renderer.domElement ); 92 | } 93 | 94 | function animate() { 95 | requestAnimationFrame( animate ); 96 | renderer.render( scene, camera ); 97 | controls.update(); 98 | } 99 | -------------------------------------------------------------------------------- /envmaps/Yokohama/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/negx.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/negy.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/negz.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/posx.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/posy.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/envmaps/Yokohama/posz.jpg -------------------------------------------------------------------------------- /envmaps/Yokohama/readme.md: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | 7 | 8 | 9 | License 10 | ======= 11 | 12 | This work is licensed under a Creative Commons Attribution 3.0 Unported License. 13 | http://creativecommons.org/licenses/by/3.0/ 14 | -------------------------------------------------------------------------------- /fire-hydrant.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Fire Hydrant Test 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | README 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /fire-hydrant.js: -------------------------------------------------------------------------------- 1 | init(); 2 | animate(); 3 | 4 | // Thanks to @cx20 for this sample code! 5 | function init() { 6 | width = window.innerWidth; 7 | height = window.innerHeight; 8 | 9 | scene = new THREE.Scene(); 10 | 11 | var ambient = new THREE.AmbientLight( 0x101030 ); 12 | scene.add( ambient ); 13 | 14 | var directionalLight = new THREE.DirectionalLight(0xffeedd); 15 | directionalLight.position.set(0.5, 0.8, 1); 16 | scene.add(directionalLight); 17 | 18 | var fillLight = new THREE.DirectionalLight(0xddeeff); 19 | fillLight.position.set(-0.5, 0.2, -1); 20 | scene.add(fillLight); 21 | 22 | // Creative Commons license, downloaded from http://www.humus.name/index.php?page=Textures 23 | var envPath = "envmaps/Yokohama/"; 24 | var urls = [ 25 | envPath + 'posx.jpg', 26 | envPath + 'negx.jpg', 27 | envPath + 'posy.jpg', 28 | envPath + 'negy.jpg', 29 | envPath + 'posz.jpg', 30 | envPath + 'negz.jpg' 31 | ]; 32 | 33 | var reflectionCube = new THREE.CubeTextureLoader().load(urls); 34 | reflectionCube.format = THREE.RGBFormat; 35 | 36 | // This line displays the reflectionCube as the scene's background. 37 | scene.background = reflectionCube; 38 | 39 | camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 200 ); 40 | camera.position.z = 300; 41 | camera.position.copy(new THREE.Vector3(0, 2, 3)); 42 | 43 | var manager = new THREE.LoadingManager(); 44 | manager.onProgress = function ( item, loaded, total ) { 45 | console.log( item, loaded, total ); 46 | }; 47 | 48 | var objLoader = new THREE.OBJLoader(manager); 49 | objLoader.load('models/FireHydrant/FireHydrantMesh.obj', function(object) { 50 | var mainMesh; 51 | object.traverse(function (child) { 52 | if (child instanceof THREE.Mesh) { 53 | mainMesh = child; 54 | } 55 | }); 56 | 57 | object.position.set(0, -2.5, 0); 58 | object.scale.set(4, 4, 4); 59 | 60 | var textureLoader = new THREE.TextureLoader(); 61 | mainMesh.material = new THREE.MeshStandardMaterial({ 62 | map: textureLoader.load('models/FireHydrant/fire_hydrant_Base_Color.png'), 63 | normalMap: textureLoader.load('models/FireHydrant/fire_hydrant_Normal_OpenGL.png'), 64 | roughnessMap: textureLoader.load('models/FireHydrant/fire_hydrant_Roughness.png'), 65 | metalnessMap: textureLoader.load('models/FireHydrant/fire_hydrant_Metallic.png'), 66 | aoMap: textureLoader.load('models/FireHydrant/fire_hydrant_Mixed_AO.png'), 67 | 68 | //roughness: 0, 69 | //metalness: 1, 70 | aoMapIntensity: 0.5, // The ao map appears to quash the reflectionCube. 71 | envMap: reflectionCube 72 | }); 73 | 74 | // The fire hydrant uses UDIM, but is able to operate without UDIM via simple 75 | // texture coordinate wrapping. This is because only symmetric parts are placed 76 | // in UDIM space, and can re-use the main texture via wrapped coordinates. 77 | // https://support.allegorithmic.com/documentation/display/SPDOC/UDIM 78 | function wrap(texture) { 79 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 80 | } 81 | wrap(mainMesh.material.map); 82 | wrap(mainMesh.material.normalMap); 83 | wrap(mainMesh.material.roughnessMap); 84 | wrap(mainMesh.material.metalnessMap); 85 | wrap(mainMesh.material.aoMap); 86 | 87 | scene.add(object); 88 | window.mainObject = object; // for debugging only 89 | window.mainMesh = mainMesh; // for debugging only 90 | }); 91 | 92 | renderer = new THREE.WebGLRenderer(); 93 | renderer.setClearColor( 0xaaaaaa ); 94 | 95 | controls = new THREE.OrbitControls( camera, renderer.domElement ); 96 | controls.userPan = false; 97 | controls.userPanSpeed = 0.0; 98 | controls.maxDistance = 5000.0; 99 | controls.maxPolarAngle = Math.PI * 0.895; 100 | controls.autoRotate = false; 101 | controls.autoRotateSpeed = -10.0; 102 | 103 | renderer.setSize( width, height ); 104 | document.body.appendChild( renderer.domElement ); 105 | } 106 | 107 | function animate() { 108 | requestAnimationFrame( animate ); 109 | renderer.render( scene, camera ); 110 | controls.update(); 111 | } 112 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: sans-serif; 3 | font-size: 16px; 4 | max-width: 800px; 5 | margin: 0 auto; 6 | } 7 | p { 8 | clear: both; 9 | } 10 | .header { 11 | padding: 0 15px; 12 | } 13 | .container { 14 | background: #eee; 15 | padding: 10px 15px; 16 | margin: 0 0 15px; 17 | } 18 | .container h2 { 19 | margin: 0 0 0.9em; 20 | } 21 | .thumbnail { 22 | display: inline-block; 23 | position: relative; 24 | width: 128px; 25 | height: 128px; 26 | background-size: contain; 27 | margin-bottom: 1.4em; 28 | } 29 | .thumbnail::after { 30 | position: absolute; 31 | top: 132px; 32 | left: 0; 33 | width: 128px; 34 | text-align: center; 35 | content: attr(data-name); 36 | } 37 | .leftBar { 38 | float: left; 39 | clear: both; 40 | margin: 0 1em 1.2em 0; 41 | } 42 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Physically Based Rendering (PBR) Test Models 6 | 7 | 8 | 9 |
10 |

Physically Based Rendering (PBR) Tests

11 | 12 |

13 | @emackey - 14 | source code 15 |

16 | 17 |

18 | These are some quick tests of PBR using 19 | THREE.MeshStandardMaterial. 21 |

22 |
23 | 24 |
25 |

Fire Hydrant - View Demo

26 | 27 |

28 | The mesh came from Substance Share 29 | and is covered by the 30 | Creative Commons 4.0 license. 31 | The textures were (mostly) procedurally generated in Substance Painter 2.3.1. The model has some visible polygon 32 | edges on the side hose hookups that were not visible in Substance Painter, but these may be artifacts from 33 | a sub-optimal conversion process I used. Also, the yellow paint looks shinier in Three.js than it did in 34 | Substance Painter. 35 |

36 | 37 |

38 |

39 |
40 |
41 |
42 |
43 |

44 |
45 | 46 |
47 |

Chest - View Demo

48 | 49 |

50 | I believe this is a demo object from Luxology Modo. A detail map was removed such that the model 51 | can work just from the primary PBR maps. 52 |

53 | 54 |

55 | Zoom in on one of the brass bumps on the front of the chest. Note that you can see the reflection 56 | of the environment in the brass, and again on the metal buckles. The reflection map is not 57 | part of the Chest model, it came from the host application's scene. I believe that glTF loaders 58 | will need to provide an API for the host app to supply a reflection map to the model being loaded. 59 | The reflection map cannot be included in a glTF model if the model is to be loaded into a larger 60 | scene. 61 |

62 | 63 |

64 |

65 |
66 |
67 |
68 |
69 |

70 |
71 | 72 |
73 |

Normal Tangent Test

74 | 75 |

76 | It's normal-mapping time. Do you know where your tangent vectors are? 77 |

78 |
79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /libs/three.js/OBJLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | THREE.OBJLoader = function ( manager ) { 6 | 7 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 8 | 9 | this.materials = null; 10 | 11 | this.regexp = { 12 | // v float float float 13 | vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 14 | // vn float float float 15 | normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 16 | // vt float float 17 | uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 18 | // f vertex vertex vertex 19 | face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, 20 | // f vertex/uv vertex/uv vertex/uv 21 | face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, 22 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 23 | face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, 24 | // f vertex//normal vertex//normal vertex//normal 25 | face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, 26 | // o object_name | g group_name 27 | object_pattern : /^[og]\s*(.+)?/, 28 | // s boolean 29 | smoothing_pattern : /^s\s+(\d+|on|off)/, 30 | // mtllib file_reference 31 | material_library_pattern : /^mtllib /, 32 | // usemtl material_name 33 | material_use_pattern : /^usemtl / 34 | }; 35 | 36 | }; 37 | 38 | THREE.OBJLoader.prototype = { 39 | 40 | constructor: THREE.OBJLoader, 41 | 42 | load: function ( url, onLoad, onProgress, onError ) { 43 | 44 | var scope = this; 45 | 46 | var loader = new THREE.XHRLoader( scope.manager ); 47 | loader.setPath( this.path ); 48 | loader.load( url, function ( text ) { 49 | 50 | onLoad( scope.parse( text ) ); 51 | 52 | }, onProgress, onError ); 53 | 54 | }, 55 | 56 | setPath: function ( value ) { 57 | 58 | this.path = value; 59 | 60 | }, 61 | 62 | setMaterials: function ( materials ) { 63 | 64 | this.materials = materials; 65 | 66 | }, 67 | 68 | _createParserState : function () { 69 | 70 | var state = { 71 | objects : [], 72 | object : {}, 73 | 74 | vertices : [], 75 | normals : [], 76 | uvs : [], 77 | 78 | materialLibraries : [], 79 | 80 | startObject: function ( name, fromDeclaration ) { 81 | 82 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 83 | // file. We need to use it for the first parsed g/o to keep things in sync. 84 | if ( this.object && this.object.fromDeclaration === false ) { 85 | 86 | this.object.name = name; 87 | this.object.fromDeclaration = ( fromDeclaration !== false ); 88 | return; 89 | 90 | } 91 | 92 | var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); 93 | 94 | if ( this.object && typeof this.object._finalize === 'function' ) { 95 | 96 | this.object._finalize( true ); 97 | 98 | } 99 | 100 | this.object = { 101 | name : name || '', 102 | fromDeclaration : ( fromDeclaration !== false ), 103 | 104 | geometry : { 105 | vertices : [], 106 | normals : [], 107 | uvs : [] 108 | }, 109 | materials : [], 110 | smooth : true, 111 | 112 | startMaterial : function( name, libraries ) { 113 | 114 | var previous = this._finalize( false ); 115 | 116 | // New usemtl declaration overwrites an inherited material, except if faces were declared 117 | // after the material, then it must be preserved for proper MultiMaterial continuation. 118 | if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { 119 | 120 | this.materials.splice( previous.index, 1 ); 121 | 122 | } 123 | 124 | var material = { 125 | index : this.materials.length, 126 | name : name || '', 127 | mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), 128 | smooth : ( previous !== undefined ? previous.smooth : this.smooth ), 129 | groupStart : ( previous !== undefined ? previous.groupEnd : 0 ), 130 | groupEnd : -1, 131 | groupCount : -1, 132 | inherited : false, 133 | 134 | clone : function( index ) { 135 | var cloned = { 136 | index : ( typeof index === 'number' ? index : this.index ), 137 | name : this.name, 138 | mtllib : this.mtllib, 139 | smooth : this.smooth, 140 | groupStart : 0, 141 | groupEnd : -1, 142 | groupCount : -1, 143 | inherited : false 144 | }; 145 | cloned.clone = this.clone.bind(cloned); 146 | return cloned; 147 | } 148 | }; 149 | 150 | this.materials.push( material ); 151 | 152 | return material; 153 | 154 | }, 155 | 156 | currentMaterial : function() { 157 | 158 | if ( this.materials.length > 0 ) { 159 | return this.materials[ this.materials.length - 1 ]; 160 | } 161 | 162 | return undefined; 163 | 164 | }, 165 | 166 | _finalize : function( end ) { 167 | 168 | var lastMultiMaterial = this.currentMaterial(); 169 | if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) { 170 | 171 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 172 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 173 | lastMultiMaterial.inherited = false; 174 | 175 | } 176 | 177 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 178 | if ( end && this.materials.length > 1 ) { 179 | 180 | for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) { 181 | if ( this.materials[mi].groupCount <= 0 ) { 182 | this.materials.splice( mi, 1 ); 183 | } 184 | } 185 | 186 | } 187 | 188 | // Guarantee at least one empty material, this makes the creation later more straight forward. 189 | if ( end && this.materials.length === 0 ) { 190 | 191 | this.materials.push({ 192 | name : '', 193 | smooth : this.smooth 194 | }); 195 | 196 | } 197 | 198 | return lastMultiMaterial; 199 | 200 | } 201 | }; 202 | 203 | // Inherit previous objects material. 204 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 205 | // If a usemtl declaration is encountered while this new object is being parsed, it will 206 | // overwrite the inherited material. Exception being that there was already face declarations 207 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 208 | 209 | if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) { 210 | 211 | var declared = previousMaterial.clone( 0 ); 212 | declared.inherited = true; 213 | this.object.materials.push( declared ); 214 | 215 | } 216 | 217 | this.objects.push( this.object ); 218 | 219 | }, 220 | 221 | finalize : function() { 222 | 223 | if ( this.object && typeof this.object._finalize === 'function' ) { 224 | 225 | this.object._finalize( true ); 226 | 227 | } 228 | 229 | }, 230 | 231 | parseVertexIndex: function ( value, len ) { 232 | 233 | var index = parseInt( value, 10 ); 234 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 235 | 236 | }, 237 | 238 | parseNormalIndex: function ( value, len ) { 239 | 240 | var index = parseInt( value, 10 ); 241 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 242 | 243 | }, 244 | 245 | parseUVIndex: function ( value, len ) { 246 | 247 | var index = parseInt( value, 10 ); 248 | return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; 249 | 250 | }, 251 | 252 | addVertex: function ( a, b, c ) { 253 | 254 | var src = this.vertices; 255 | var dst = this.object.geometry.vertices; 256 | 257 | dst.push( src[ a + 0 ] ); 258 | dst.push( src[ a + 1 ] ); 259 | dst.push( src[ a + 2 ] ); 260 | dst.push( src[ b + 0 ] ); 261 | dst.push( src[ b + 1 ] ); 262 | dst.push( src[ b + 2 ] ); 263 | dst.push( src[ c + 0 ] ); 264 | dst.push( src[ c + 1 ] ); 265 | dst.push( src[ c + 2 ] ); 266 | 267 | }, 268 | 269 | addVertexLine: function ( a ) { 270 | 271 | var src = this.vertices; 272 | var dst = this.object.geometry.vertices; 273 | 274 | dst.push( src[ a + 0 ] ); 275 | dst.push( src[ a + 1 ] ); 276 | dst.push( src[ a + 2 ] ); 277 | 278 | }, 279 | 280 | addNormal : function ( a, b, c ) { 281 | 282 | var src = this.normals; 283 | var dst = this.object.geometry.normals; 284 | 285 | dst.push( src[ a + 0 ] ); 286 | dst.push( src[ a + 1 ] ); 287 | dst.push( src[ a + 2 ] ); 288 | dst.push( src[ b + 0 ] ); 289 | dst.push( src[ b + 1 ] ); 290 | dst.push( src[ b + 2 ] ); 291 | dst.push( src[ c + 0 ] ); 292 | dst.push( src[ c + 1 ] ); 293 | dst.push( src[ c + 2 ] ); 294 | 295 | }, 296 | 297 | addUV: function ( a, b, c ) { 298 | 299 | var src = this.uvs; 300 | var dst = this.object.geometry.uvs; 301 | 302 | dst.push( src[ a + 0 ] ); 303 | dst.push( src[ a + 1 ] ); 304 | dst.push( src[ b + 0 ] ); 305 | dst.push( src[ b + 1 ] ); 306 | dst.push( src[ c + 0 ] ); 307 | dst.push( src[ c + 1 ] ); 308 | 309 | }, 310 | 311 | addUVLine: function ( a ) { 312 | 313 | var src = this.uvs; 314 | var dst = this.object.geometry.uvs; 315 | 316 | dst.push( src[ a + 0 ] ); 317 | dst.push( src[ a + 1 ] ); 318 | 319 | }, 320 | 321 | addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 322 | 323 | var vLen = this.vertices.length; 324 | 325 | var ia = this.parseVertexIndex( a, vLen ); 326 | var ib = this.parseVertexIndex( b, vLen ); 327 | var ic = this.parseVertexIndex( c, vLen ); 328 | var id; 329 | 330 | if ( d === undefined ) { 331 | 332 | this.addVertex( ia, ib, ic ); 333 | 334 | } else { 335 | 336 | id = this.parseVertexIndex( d, vLen ); 337 | 338 | this.addVertex( ia, ib, id ); 339 | this.addVertex( ib, ic, id ); 340 | 341 | } 342 | 343 | if ( ua !== undefined ) { 344 | 345 | var uvLen = this.uvs.length; 346 | 347 | ia = this.parseUVIndex( ua, uvLen ); 348 | ib = this.parseUVIndex( ub, uvLen ); 349 | ic = this.parseUVIndex( uc, uvLen ); 350 | 351 | if ( d === undefined ) { 352 | 353 | this.addUV( ia, ib, ic ); 354 | 355 | } else { 356 | 357 | id = this.parseUVIndex( ud, uvLen ); 358 | 359 | this.addUV( ia, ib, id ); 360 | this.addUV( ib, ic, id ); 361 | 362 | } 363 | 364 | } 365 | 366 | if ( na !== undefined ) { 367 | 368 | // Normals are many times the same. If so, skip function call and parseInt. 369 | var nLen = this.normals.length; 370 | ia = this.parseNormalIndex( na, nLen ); 371 | 372 | ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); 373 | ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); 374 | 375 | if ( d === undefined ) { 376 | 377 | this.addNormal( ia, ib, ic ); 378 | 379 | } else { 380 | 381 | id = this.parseNormalIndex( nd, nLen ); 382 | 383 | this.addNormal( ia, ib, id ); 384 | this.addNormal( ib, ic, id ); 385 | 386 | } 387 | 388 | } 389 | 390 | }, 391 | 392 | addLineGeometry: function ( vertices, uvs ) { 393 | 394 | this.object.geometry.type = 'Line'; 395 | 396 | var vLen = this.vertices.length; 397 | var uvLen = this.uvs.length; 398 | 399 | for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { 400 | 401 | this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); 402 | 403 | } 404 | 405 | for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { 406 | 407 | this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); 408 | 409 | } 410 | 411 | } 412 | 413 | }; 414 | 415 | state.startObject( '', false ); 416 | 417 | return state; 418 | 419 | }, 420 | 421 | parse: function ( text ) { 422 | 423 | console.time( 'OBJLoader' ); 424 | 425 | var state = this._createParserState(); 426 | 427 | if ( text.indexOf( '\r\n' ) !== - 1 ) { 428 | 429 | // This is faster than String.split with regex that splits on both 430 | text = text.replace( /\r\n/g, '\n' ); 431 | 432 | } 433 | 434 | if ( text.indexOf( '\\\n' ) !== - 1) { 435 | 436 | // join lines separated by a line continuation character (\) 437 | text = text.replace( /\\\n/g, '' ); 438 | 439 | } 440 | 441 | var lines = text.split( '\n' ); 442 | var line = '', lineFirstChar = '', lineSecondChar = ''; 443 | var lineLength = 0; 444 | var result = []; 445 | 446 | // Faster to just trim left side of the line. Use if available. 447 | var trimLeft = ( typeof ''.trimLeft === 'function' ); 448 | 449 | for ( var i = 0, l = lines.length; i < l; i ++ ) { 450 | 451 | line = lines[ i ]; 452 | 453 | line = trimLeft ? line.trimLeft() : line.trim(); 454 | 455 | lineLength = line.length; 456 | 457 | if ( lineLength === 0 ) continue; 458 | 459 | lineFirstChar = line.charAt( 0 ); 460 | 461 | // @todo invoke passed in handler if any 462 | if ( lineFirstChar === '#' ) continue; 463 | 464 | if ( lineFirstChar === 'v' ) { 465 | 466 | lineSecondChar = line.charAt( 1 ); 467 | 468 | if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) { 469 | 470 | // 0 1 2 3 471 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 472 | 473 | state.vertices.push( 474 | parseFloat( result[ 1 ] ), 475 | parseFloat( result[ 2 ] ), 476 | parseFloat( result[ 3 ] ) 477 | ); 478 | 479 | } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) { 480 | 481 | // 0 1 2 3 482 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 483 | 484 | state.normals.push( 485 | parseFloat( result[ 1 ] ), 486 | parseFloat( result[ 2 ] ), 487 | parseFloat( result[ 3 ] ) 488 | ); 489 | 490 | } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) { 491 | 492 | // 0 1 2 493 | // ["vt 0.1 0.2", "0.1", "0.2"] 494 | 495 | state.uvs.push( 496 | parseFloat( result[ 1 ] ), 497 | parseFloat( result[ 2 ] ) 498 | ); 499 | 500 | } else { 501 | 502 | throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" ); 503 | 504 | } 505 | 506 | } else if ( lineFirstChar === "f" ) { 507 | 508 | if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) { 509 | 510 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 511 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 512 | // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] 513 | 514 | state.addFace( 515 | result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ], 516 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 517 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 518 | ); 519 | 520 | } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) { 521 | 522 | // f vertex/uv vertex/uv vertex/uv 523 | // 0 1 2 3 4 5 6 7 8 524 | // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] 525 | 526 | state.addFace( 527 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 528 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 529 | ); 530 | 531 | } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) { 532 | 533 | // f vertex//normal vertex//normal vertex//normal 534 | // 0 1 2 3 4 5 6 7 8 535 | // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] 536 | 537 | state.addFace( 538 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 539 | undefined, undefined, undefined, undefined, 540 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 541 | ); 542 | 543 | } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) { 544 | 545 | // f vertex vertex vertex 546 | // 0 1 2 3 4 547 | // ["f 1 2 3", "1", "2", "3", undefined] 548 | 549 | state.addFace( 550 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 551 | ); 552 | 553 | } else { 554 | 555 | throw new Error( "Unexpected face line: '" + line + "'" ); 556 | 557 | } 558 | 559 | } else if ( lineFirstChar === "l" ) { 560 | 561 | var lineParts = line.substring( 1 ).trim().split( " " ); 562 | var lineVertices = [], lineUVs = []; 563 | 564 | if ( line.indexOf( "/" ) === - 1 ) { 565 | 566 | lineVertices = lineParts; 567 | 568 | } else { 569 | 570 | for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { 571 | 572 | var parts = lineParts[ li ].split( "/" ); 573 | 574 | if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); 575 | if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); 576 | 577 | } 578 | 579 | } 580 | state.addLineGeometry( lineVertices, lineUVs ); 581 | 582 | } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) { 583 | 584 | // o object_name 585 | // or 586 | // g group_name 587 | 588 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 589 | // var name = result[ 0 ].substr( 1 ).trim(); 590 | var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); 591 | 592 | state.startObject( name ); 593 | 594 | } else if ( this.regexp.material_use_pattern.test( line ) ) { 595 | 596 | // material 597 | 598 | state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); 599 | 600 | } else if ( this.regexp.material_library_pattern.test( line ) ) { 601 | 602 | // mtl file 603 | 604 | state.materialLibraries.push( line.substring( 7 ).trim() ); 605 | 606 | } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) { 607 | 608 | // smooth shading 609 | 610 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 611 | // but does not define a usemtl for each face set. 612 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 613 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 614 | // where explicit usemtl defines geometry groups. 615 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 616 | 617 | var value = result[ 1 ].trim().toLowerCase(); 618 | state.object.smooth = ( value === '1' || value === 'on' ); 619 | 620 | var material = state.object.currentMaterial(); 621 | if ( material ) { 622 | 623 | material.smooth = state.object.smooth; 624 | 625 | } 626 | 627 | } else { 628 | 629 | // Handle null terminated files without exception 630 | if ( line === '\0' ) continue; 631 | 632 | throw new Error( "Unexpected line: '" + line + "'" ); 633 | 634 | } 635 | 636 | } 637 | 638 | state.finalize(); 639 | 640 | var container = new THREE.Group(); 641 | container.materialLibraries = [].concat( state.materialLibraries ); 642 | 643 | for ( var i = 0, l = state.objects.length; i < l; i ++ ) { 644 | 645 | var object = state.objects[ i ]; 646 | var geometry = object.geometry; 647 | var materials = object.materials; 648 | var isLine = ( geometry.type === 'Line' ); 649 | 650 | // Skip o/g line declarations that did not follow with any faces 651 | if ( geometry.vertices.length === 0 ) continue; 652 | 653 | var buffergeometry = new THREE.BufferGeometry(); 654 | 655 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 656 | 657 | if ( geometry.normals.length > 0 ) { 658 | 659 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 660 | 661 | } else { 662 | 663 | buffergeometry.computeVertexNormals(); 664 | 665 | } 666 | 667 | if ( geometry.uvs.length > 0 ) { 668 | 669 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 670 | 671 | } 672 | 673 | // Create materials 674 | 675 | var createdMaterials = []; 676 | 677 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 678 | 679 | var sourceMaterial = materials[mi]; 680 | var material = undefined; 681 | 682 | if ( this.materials !== null ) { 683 | 684 | material = this.materials.create( sourceMaterial.name ); 685 | 686 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 687 | if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { 688 | 689 | var materialLine = new THREE.LineBasicMaterial(); 690 | materialLine.copy( material ); 691 | material = materialLine; 692 | 693 | } 694 | 695 | } 696 | 697 | if ( ! material ) { 698 | 699 | material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); 700 | material.name = sourceMaterial.name; 701 | 702 | } 703 | 704 | material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; 705 | 706 | createdMaterials.push(material); 707 | 708 | } 709 | 710 | // Create mesh 711 | 712 | var mesh; 713 | 714 | if ( createdMaterials.length > 1 ) { 715 | 716 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 717 | 718 | var sourceMaterial = materials[mi]; 719 | buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); 720 | 721 | } 722 | 723 | var multiMaterial = new THREE.MultiMaterial( createdMaterials ); 724 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, multiMaterial ) : new THREE.LineSegments( buffergeometry, multiMaterial ) ); 725 | 726 | } else { 727 | 728 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); 729 | } 730 | 731 | mesh.name = object.name; 732 | 733 | container.add( mesh ); 734 | 735 | } 736 | 737 | console.timeEnd( 'OBJLoader' ); 738 | 739 | return container; 740 | 741 | } 742 | 743 | }; 744 | -------------------------------------------------------------------------------- /libs/three.js/r82dev/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 14 | // Pan - right mouse, or arrow keys / touch: three finter swipe 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 63 | 64 | // Set to true to automatically rotate around the target 65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 66 | this.autoRotate = false; 67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 68 | 69 | // Set to false to disable use of the keys 70 | this.enableKeys = true; 71 | 72 | // The four arrow keys 73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 74 | 75 | // Mouse buttons 76 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 77 | 78 | // for reset 79 | this.target0 = this.target.clone(); 80 | this.position0 = this.object.position.clone(); 81 | this.zoom0 = this.object.zoom; 82 | 83 | // 84 | // public methods 85 | // 86 | 87 | this.getPolarAngle = function () { 88 | 89 | return spherical.phi; 90 | 91 | }; 92 | 93 | this.getAzimuthalAngle = function () { 94 | 95 | return spherical.theta; 96 | 97 | }; 98 | 99 | this.reset = function () { 100 | 101 | scope.target.copy( scope.target0 ); 102 | scope.object.position.copy( scope.position0 ); 103 | scope.object.zoom = scope.zoom0; 104 | 105 | scope.object.updateProjectionMatrix(); 106 | scope.dispatchEvent( changeEvent ); 107 | 108 | scope.update(); 109 | 110 | state = STATE.NONE; 111 | 112 | }; 113 | 114 | // this method is exposed, but perhaps it would be better if we can make it private... 115 | this.update = function() { 116 | 117 | var offset = new THREE.Vector3(); 118 | 119 | // so camera.up is the orbit axis 120 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 121 | var quatInverse = quat.clone().inverse(); 122 | 123 | var lastPosition = new THREE.Vector3(); 124 | var lastQuaternion = new THREE.Quaternion(); 125 | 126 | return function update () { 127 | 128 | var position = scope.object.position; 129 | 130 | offset.copy( position ).sub( scope.target ); 131 | 132 | // rotate offset to "y-axis-is-up" space 133 | offset.applyQuaternion( quat ); 134 | 135 | // angle from z-axis around y-axis 136 | spherical.setFromVector3( offset ); 137 | 138 | if ( scope.autoRotate && state === STATE.NONE ) { 139 | 140 | rotateLeft( getAutoRotationAngle() ); 141 | 142 | } 143 | 144 | spherical.theta += sphericalDelta.theta; 145 | spherical.phi += sphericalDelta.phi; 146 | 147 | // restrict theta to be between desired limits 148 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 149 | 150 | // restrict phi to be between desired limits 151 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 152 | 153 | spherical.makeSafe(); 154 | 155 | 156 | spherical.radius *= scale; 157 | 158 | // restrict radius to be between desired limits 159 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 160 | 161 | // move target to panned location 162 | scope.target.add( panOffset ); 163 | 164 | offset.setFromSpherical( spherical ); 165 | 166 | // rotate offset back to "camera-up-vector-is-up" space 167 | offset.applyQuaternion( quatInverse ); 168 | 169 | position.copy( scope.target ).add( offset ); 170 | 171 | scope.object.lookAt( scope.target ); 172 | 173 | if ( scope.enableDamping === true ) { 174 | 175 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 176 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 177 | 178 | } else { 179 | 180 | sphericalDelta.set( 0, 0, 0 ); 181 | 182 | } 183 | 184 | scale = 1; 185 | panOffset.set( 0, 0, 0 ); 186 | 187 | // update condition is: 188 | // min(camera displacement, camera rotation in radians)^2 > EPS 189 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 190 | 191 | if ( zoomChanged || 192 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 193 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 194 | 195 | scope.dispatchEvent( changeEvent ); 196 | 197 | lastPosition.copy( scope.object.position ); 198 | lastQuaternion.copy( scope.object.quaternion ); 199 | zoomChanged = false; 200 | 201 | return true; 202 | 203 | } 204 | 205 | return false; 206 | 207 | }; 208 | 209 | }(); 210 | 211 | this.dispose = function() { 212 | 213 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 214 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 215 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 216 | 217 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 218 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 219 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 220 | 221 | document.removeEventListener( 'mousemove', onMouseMove, false ); 222 | document.removeEventListener( 'mouseup', onMouseUp, false ); 223 | 224 | window.removeEventListener( 'keydown', onKeyDown, false ); 225 | 226 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 227 | 228 | }; 229 | 230 | // 231 | // internals 232 | // 233 | 234 | var scope = this; 235 | 236 | var changeEvent = { type: 'change' }; 237 | var startEvent = { type: 'start' }; 238 | var endEvent = { type: 'end' }; 239 | 240 | var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 241 | 242 | var state = STATE.NONE; 243 | 244 | var EPS = 0.000001; 245 | 246 | // current position in spherical coordinates 247 | var spherical = new THREE.Spherical(); 248 | var sphericalDelta = new THREE.Spherical(); 249 | 250 | var scale = 1; 251 | var panOffset = new THREE.Vector3(); 252 | var zoomChanged = false; 253 | 254 | var rotateStart = new THREE.Vector2(); 255 | var rotateEnd = new THREE.Vector2(); 256 | var rotateDelta = new THREE.Vector2(); 257 | 258 | var panStart = new THREE.Vector2(); 259 | var panEnd = new THREE.Vector2(); 260 | var panDelta = new THREE.Vector2(); 261 | 262 | var dollyStart = new THREE.Vector2(); 263 | var dollyEnd = new THREE.Vector2(); 264 | var dollyDelta = new THREE.Vector2(); 265 | 266 | function getAutoRotationAngle() { 267 | 268 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 269 | 270 | } 271 | 272 | function getZoomScale() { 273 | 274 | return Math.pow( 0.95, scope.zoomSpeed ); 275 | 276 | } 277 | 278 | function rotateLeft( angle ) { 279 | 280 | sphericalDelta.theta -= angle; 281 | 282 | } 283 | 284 | function rotateUp( angle ) { 285 | 286 | sphericalDelta.phi -= angle; 287 | 288 | } 289 | 290 | var panLeft = function() { 291 | 292 | var v = new THREE.Vector3(); 293 | 294 | return function panLeft( distance, objectMatrix ) { 295 | 296 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 297 | v.multiplyScalar( - distance ); 298 | 299 | panOffset.add( v ); 300 | 301 | }; 302 | 303 | }(); 304 | 305 | var panUp = function() { 306 | 307 | var v = new THREE.Vector3(); 308 | 309 | return function panUp( distance, objectMatrix ) { 310 | 311 | v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix 312 | v.multiplyScalar( distance ); 313 | 314 | panOffset.add( v ); 315 | 316 | }; 317 | 318 | }(); 319 | 320 | // deltaX and deltaY are in pixels; right and down are positive 321 | var pan = function() { 322 | 323 | var offset = new THREE.Vector3(); 324 | 325 | return function pan ( deltaX, deltaY ) { 326 | 327 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 328 | 329 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 330 | 331 | // perspective 332 | var position = scope.object.position; 333 | offset.copy( position ).sub( scope.target ); 334 | var targetDistance = offset.length(); 335 | 336 | // half of the fov is center to top of screen 337 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 338 | 339 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 340 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 341 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 342 | 343 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 344 | 345 | // orthographic 346 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 347 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 348 | 349 | } else { 350 | 351 | // camera neither orthographic nor perspective 352 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 353 | scope.enablePan = false; 354 | 355 | } 356 | 357 | }; 358 | 359 | }(); 360 | 361 | function dollyIn( dollyScale ) { 362 | 363 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 364 | 365 | scale /= dollyScale; 366 | 367 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 368 | 369 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 370 | scope.object.updateProjectionMatrix(); 371 | zoomChanged = true; 372 | 373 | } else { 374 | 375 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 376 | scope.enableZoom = false; 377 | 378 | } 379 | 380 | } 381 | 382 | function dollyOut( dollyScale ) { 383 | 384 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 385 | 386 | scale *= dollyScale; 387 | 388 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 389 | 390 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 391 | scope.object.updateProjectionMatrix(); 392 | zoomChanged = true; 393 | 394 | } else { 395 | 396 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 397 | scope.enableZoom = false; 398 | 399 | } 400 | 401 | } 402 | 403 | // 404 | // event callbacks - update the object state 405 | // 406 | 407 | function handleMouseDownRotate( event ) { 408 | 409 | //console.log( 'handleMouseDownRotate' ); 410 | 411 | rotateStart.set( event.clientX, event.clientY ); 412 | 413 | } 414 | 415 | function handleMouseDownDolly( event ) { 416 | 417 | //console.log( 'handleMouseDownDolly' ); 418 | 419 | dollyStart.set( event.clientX, event.clientY ); 420 | 421 | } 422 | 423 | function handleMouseDownPan( event ) { 424 | 425 | //console.log( 'handleMouseDownPan' ); 426 | 427 | panStart.set( event.clientX, event.clientY ); 428 | 429 | } 430 | 431 | function handleMouseMoveRotate( event ) { 432 | 433 | //console.log( 'handleMouseMoveRotate' ); 434 | 435 | rotateEnd.set( event.clientX, event.clientY ); 436 | rotateDelta.subVectors( rotateEnd, rotateStart ); 437 | 438 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 439 | 440 | // rotating across whole screen goes 360 degrees around 441 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 442 | 443 | // rotating up and down along whole screen attempts to go 360, but limited to 180 444 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 445 | 446 | rotateStart.copy( rotateEnd ); 447 | 448 | scope.update(); 449 | 450 | } 451 | 452 | function handleMouseMoveDolly( event ) { 453 | 454 | //console.log( 'handleMouseMoveDolly' ); 455 | 456 | dollyEnd.set( event.clientX, event.clientY ); 457 | 458 | dollyDelta.subVectors( dollyEnd, dollyStart ); 459 | 460 | if ( dollyDelta.y > 0 ) { 461 | 462 | dollyIn( getZoomScale() ); 463 | 464 | } else if ( dollyDelta.y < 0 ) { 465 | 466 | dollyOut( getZoomScale() ); 467 | 468 | } 469 | 470 | dollyStart.copy( dollyEnd ); 471 | 472 | scope.update(); 473 | 474 | } 475 | 476 | function handleMouseMovePan( event ) { 477 | 478 | //console.log( 'handleMouseMovePan' ); 479 | 480 | panEnd.set( event.clientX, event.clientY ); 481 | 482 | panDelta.subVectors( panEnd, panStart ); 483 | 484 | pan( panDelta.x, panDelta.y ); 485 | 486 | panStart.copy( panEnd ); 487 | 488 | scope.update(); 489 | 490 | } 491 | 492 | function handleMouseUp( event ) { 493 | 494 | //console.log( 'handleMouseUp' ); 495 | 496 | } 497 | 498 | function handleMouseWheel( event ) { 499 | 500 | //console.log( 'handleMouseWheel' ); 501 | 502 | if ( event.deltaY < 0 ) { 503 | 504 | dollyOut( getZoomScale() ); 505 | 506 | } else if ( event.deltaY > 0 ) { 507 | 508 | dollyIn( getZoomScale() ); 509 | 510 | } 511 | 512 | scope.update(); 513 | 514 | } 515 | 516 | function handleKeyDown( event ) { 517 | 518 | //console.log( 'handleKeyDown' ); 519 | 520 | switch ( event.keyCode ) { 521 | 522 | case scope.keys.UP: 523 | pan( 0, scope.keyPanSpeed ); 524 | scope.update(); 525 | break; 526 | 527 | case scope.keys.BOTTOM: 528 | pan( 0, - scope.keyPanSpeed ); 529 | scope.update(); 530 | break; 531 | 532 | case scope.keys.LEFT: 533 | pan( scope.keyPanSpeed, 0 ); 534 | scope.update(); 535 | break; 536 | 537 | case scope.keys.RIGHT: 538 | pan( - scope.keyPanSpeed, 0 ); 539 | scope.update(); 540 | break; 541 | 542 | } 543 | 544 | } 545 | 546 | function handleTouchStartRotate( event ) { 547 | 548 | //console.log( 'handleTouchStartRotate' ); 549 | 550 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 551 | 552 | } 553 | 554 | function handleTouchStartDolly( event ) { 555 | 556 | //console.log( 'handleTouchStartDolly' ); 557 | 558 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 559 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 560 | 561 | var distance = Math.sqrt( dx * dx + dy * dy ); 562 | 563 | dollyStart.set( 0, distance ); 564 | 565 | } 566 | 567 | function handleTouchStartPan( event ) { 568 | 569 | //console.log( 'handleTouchStartPan' ); 570 | 571 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 572 | 573 | } 574 | 575 | function handleTouchMoveRotate( event ) { 576 | 577 | //console.log( 'handleTouchMoveRotate' ); 578 | 579 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 580 | rotateDelta.subVectors( rotateEnd, rotateStart ); 581 | 582 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 583 | 584 | // rotating across whole screen goes 360 degrees around 585 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 586 | 587 | // rotating up and down along whole screen attempts to go 360, but limited to 180 588 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 589 | 590 | rotateStart.copy( rotateEnd ); 591 | 592 | scope.update(); 593 | 594 | } 595 | 596 | function handleTouchMoveDolly( event ) { 597 | 598 | //console.log( 'handleTouchMoveDolly' ); 599 | 600 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 601 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 602 | 603 | var distance = Math.sqrt( dx * dx + dy * dy ); 604 | 605 | dollyEnd.set( 0, distance ); 606 | 607 | dollyDelta.subVectors( dollyEnd, dollyStart ); 608 | 609 | if ( dollyDelta.y > 0 ) { 610 | 611 | dollyOut( getZoomScale() ); 612 | 613 | } else if ( dollyDelta.y < 0 ) { 614 | 615 | dollyIn( getZoomScale() ); 616 | 617 | } 618 | 619 | dollyStart.copy( dollyEnd ); 620 | 621 | scope.update(); 622 | 623 | } 624 | 625 | function handleTouchMovePan( event ) { 626 | 627 | //console.log( 'handleTouchMovePan' ); 628 | 629 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 630 | 631 | panDelta.subVectors( panEnd, panStart ); 632 | 633 | pan( panDelta.x, panDelta.y ); 634 | 635 | panStart.copy( panEnd ); 636 | 637 | scope.update(); 638 | 639 | } 640 | 641 | function handleTouchEnd( event ) { 642 | 643 | //console.log( 'handleTouchEnd' ); 644 | 645 | } 646 | 647 | // 648 | // event handlers - FSM: listen for events and reset state 649 | // 650 | 651 | function onMouseDown( event ) { 652 | 653 | if ( scope.enabled === false ) return; 654 | 655 | event.preventDefault(); 656 | 657 | if ( event.button === scope.mouseButtons.ORBIT ) { 658 | 659 | if ( scope.enableRotate === false ) return; 660 | 661 | handleMouseDownRotate( event ); 662 | 663 | state = STATE.ROTATE; 664 | 665 | } else if ( event.button === scope.mouseButtons.ZOOM ) { 666 | 667 | if ( scope.enableZoom === false ) return; 668 | 669 | handleMouseDownDolly( event ); 670 | 671 | state = STATE.DOLLY; 672 | 673 | } else if ( event.button === scope.mouseButtons.PAN ) { 674 | 675 | if ( scope.enablePan === false ) return; 676 | 677 | handleMouseDownPan( event ); 678 | 679 | state = STATE.PAN; 680 | 681 | } 682 | 683 | if ( state !== STATE.NONE ) { 684 | 685 | document.addEventListener( 'mousemove', onMouseMove, false ); 686 | document.addEventListener( 'mouseup', onMouseUp, false ); 687 | 688 | scope.dispatchEvent( startEvent ); 689 | 690 | } 691 | 692 | } 693 | 694 | function onMouseMove( event ) { 695 | 696 | if ( scope.enabled === false ) return; 697 | 698 | event.preventDefault(); 699 | 700 | if ( state === STATE.ROTATE ) { 701 | 702 | if ( scope.enableRotate === false ) return; 703 | 704 | handleMouseMoveRotate( event ); 705 | 706 | } else if ( state === STATE.DOLLY ) { 707 | 708 | if ( scope.enableZoom === false ) return; 709 | 710 | handleMouseMoveDolly( event ); 711 | 712 | } else if ( state === STATE.PAN ) { 713 | 714 | if ( scope.enablePan === false ) return; 715 | 716 | handleMouseMovePan( event ); 717 | 718 | } 719 | 720 | } 721 | 722 | function onMouseUp( event ) { 723 | 724 | if ( scope.enabled === false ) return; 725 | 726 | handleMouseUp( event ); 727 | 728 | document.removeEventListener( 'mousemove', onMouseMove, false ); 729 | document.removeEventListener( 'mouseup', onMouseUp, false ); 730 | 731 | scope.dispatchEvent( endEvent ); 732 | 733 | state = STATE.NONE; 734 | 735 | } 736 | 737 | function onMouseWheel( event ) { 738 | 739 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 740 | 741 | event.preventDefault(); 742 | event.stopPropagation(); 743 | 744 | handleMouseWheel( event ); 745 | 746 | scope.dispatchEvent( startEvent ); // not sure why these are here... 747 | scope.dispatchEvent( endEvent ); 748 | 749 | } 750 | 751 | function onKeyDown( event ) { 752 | 753 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 754 | 755 | handleKeyDown( event ); 756 | 757 | } 758 | 759 | function onTouchStart( event ) { 760 | 761 | if ( scope.enabled === false ) return; 762 | 763 | switch ( event.touches.length ) { 764 | 765 | case 1: // one-fingered touch: rotate 766 | 767 | if ( scope.enableRotate === false ) return; 768 | 769 | handleTouchStartRotate( event ); 770 | 771 | state = STATE.TOUCH_ROTATE; 772 | 773 | break; 774 | 775 | case 2: // two-fingered touch: dolly 776 | 777 | if ( scope.enableZoom === false ) return; 778 | 779 | handleTouchStartDolly( event ); 780 | 781 | state = STATE.TOUCH_DOLLY; 782 | 783 | break; 784 | 785 | case 3: // three-fingered touch: pan 786 | 787 | if ( scope.enablePan === false ) return; 788 | 789 | handleTouchStartPan( event ); 790 | 791 | state = STATE.TOUCH_PAN; 792 | 793 | break; 794 | 795 | default: 796 | 797 | state = STATE.NONE; 798 | 799 | } 800 | 801 | if ( state !== STATE.NONE ) { 802 | 803 | scope.dispatchEvent( startEvent ); 804 | 805 | } 806 | 807 | } 808 | 809 | function onTouchMove( event ) { 810 | 811 | if ( scope.enabled === false ) return; 812 | 813 | event.preventDefault(); 814 | event.stopPropagation(); 815 | 816 | switch ( event.touches.length ) { 817 | 818 | case 1: // one-fingered touch: rotate 819 | 820 | if ( scope.enableRotate === false ) return; 821 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... 822 | 823 | handleTouchMoveRotate( event ); 824 | 825 | break; 826 | 827 | case 2: // two-fingered touch: dolly 828 | 829 | if ( scope.enableZoom === false ) return; 830 | if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... 831 | 832 | handleTouchMoveDolly( event ); 833 | 834 | break; 835 | 836 | case 3: // three-fingered touch: pan 837 | 838 | if ( scope.enablePan === false ) return; 839 | if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... 840 | 841 | handleTouchMovePan( event ); 842 | 843 | break; 844 | 845 | default: 846 | 847 | state = STATE.NONE; 848 | 849 | } 850 | 851 | } 852 | 853 | function onTouchEnd( event ) { 854 | 855 | if ( scope.enabled === false ) return; 856 | 857 | handleTouchEnd( event ); 858 | 859 | scope.dispatchEvent( endEvent ); 860 | 861 | state = STATE.NONE; 862 | 863 | } 864 | 865 | function onContextMenu( event ) { 866 | 867 | event.preventDefault(); 868 | 869 | } 870 | 871 | // 872 | 873 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 874 | 875 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 876 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 877 | 878 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 879 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 880 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 881 | 882 | window.addEventListener( 'keydown', onKeyDown, false ); 883 | 884 | // force an update at start 885 | 886 | this.update(); 887 | 888 | }; 889 | 890 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 891 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 892 | 893 | Object.defineProperties( THREE.OrbitControls.prototype, { 894 | 895 | center: { 896 | 897 | get: function () { 898 | 899 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 900 | return this.target; 901 | 902 | } 903 | 904 | }, 905 | 906 | // backward compatibility 907 | 908 | noZoom: { 909 | 910 | get: function () { 911 | 912 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 913 | return ! this.enableZoom; 914 | 915 | }, 916 | 917 | set: function ( value ) { 918 | 919 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 920 | this.enableZoom = ! value; 921 | 922 | } 923 | 924 | }, 925 | 926 | noRotate: { 927 | 928 | get: function () { 929 | 930 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 931 | return ! this.enableRotate; 932 | 933 | }, 934 | 935 | set: function ( value ) { 936 | 937 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 938 | this.enableRotate = ! value; 939 | 940 | } 941 | 942 | }, 943 | 944 | noPan: { 945 | 946 | get: function () { 947 | 948 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 949 | return ! this.enablePan; 950 | 951 | }, 952 | 953 | set: function ( value ) { 954 | 955 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 956 | this.enablePan = ! value; 957 | 958 | } 959 | 960 | }, 961 | 962 | noKeys: { 963 | 964 | get: function () { 965 | 966 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 967 | return ! this.enableKeys; 968 | 969 | }, 970 | 971 | set: function ( value ) { 972 | 973 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 974 | this.enableKeys = ! value; 975 | 976 | } 977 | 978 | }, 979 | 980 | staticMoving : { 981 | 982 | get: function () { 983 | 984 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 985 | return ! this.enableDamping; 986 | 987 | }, 988 | 989 | set: function ( value ) { 990 | 991 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 992 | this.enableDamping = ! value; 993 | 994 | } 995 | 996 | }, 997 | 998 | dynamicDampingFactor : { 999 | 1000 | get: function () { 1001 | 1002 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1003 | return this.dampingFactor; 1004 | 1005 | }, 1006 | 1007 | set: function ( value ) { 1008 | 1009 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1010 | this.dampingFactor = value; 1011 | 1012 | } 1013 | 1014 | } 1015 | 1016 | } ); 1017 | -------------------------------------------------------------------------------- /models/Chest/chest_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/Chest/chest_albedo.png -------------------------------------------------------------------------------- /models/Chest/chest_ao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/Chest/chest_ao.png -------------------------------------------------------------------------------- /models/Chest/chest_metalness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/Chest/chest_metalness.png -------------------------------------------------------------------------------- /models/Chest/chest_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/Chest/chest_normal.png -------------------------------------------------------------------------------- /models/Chest/chest_roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/Chest/chest_roughness.png -------------------------------------------------------------------------------- /models/FireHydrant/fire_hydrant_Base_Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/FireHydrant/fire_hydrant_Base_Color.png -------------------------------------------------------------------------------- /models/FireHydrant/fire_hydrant_Metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/FireHydrant/fire_hydrant_Metallic.png -------------------------------------------------------------------------------- /models/FireHydrant/fire_hydrant_Mixed_AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/FireHydrant/fire_hydrant_Mixed_AO.png -------------------------------------------------------------------------------- /models/FireHydrant/fire_hydrant_Normal_OpenGL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/FireHydrant/fire_hydrant_Normal_OpenGL.png -------------------------------------------------------------------------------- /models/FireHydrant/fire_hydrant_Roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/FireHydrant/fire_hydrant_Roughness.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTestPlatform.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'NormalTangentTestPlatform.blend' 2 | # Material Count: 1 3 | 4 | newmtl Material 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.640000 0.640000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTestPlatform_high.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'NormalTangentTestPlatform.blend' 2 | # Material Count: 1 3 | 4 | newmtl Material 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.640000 0.640000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_BaseColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_BaseColor.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_Metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_Metallic.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_Normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_Normal.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_Occlusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_Occlusion.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_OcclusionRoughnessMetallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_OcclusionRoughnessMetallic.png -------------------------------------------------------------------------------- /models/NormalTangentTest/NormalTangentTest_Roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/NormalTangentTest_Roughness.png -------------------------------------------------------------------------------- /models/NormalTangentTest/UV_label_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/models/NormalTangentTest/UV_label_alpha.png -------------------------------------------------------------------------------- /normal-tangent-readme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Normal-Tangent Test 6 | 7 | 8 | 9 |
10 |

Normal-Tangent Test

11 | 12 |

13 | @emackey - 14 | source code - 15 | other pbr models 16 |

17 | 18 |

19 | It's normal-mapping time. Do you know where your tangent vectors are? 20 |

21 |
22 | 23 |
24 |

View Demo

25 | 26 |

27 | I created this model to test how normal, tangent, and bi-tangent vectors behave in various 28 | applications and engines. The tiles on the left are hemisphere-on-plane shaped geometry, 29 | and the tiles to the right of those are individual quads, which will contain normal-mapped 30 | versions of the same geometry (as calculated by Substance Painter 2.4). 31 | I've spun the UV maps around in 5 different directions, 32 | to cause the tangent vectors to go all different ways. 33 |

34 | 35 |

36 | Example UV directions 37 | Example UV positions 38 |

39 | 40 |

41 | I don't expect the normal-mapped versions to work off-axis, as the hemisphere 42 | geometry is quite large: 43 |

44 | 45 |

46 | Example of off-axis 47 |

48 | 49 |

50 | But, when viewed directly face-on (orthographic or nearly so), I expect the normal map 51 | to closely replicate the geometry, regardless of the orientation of the 52 | normal-mapped quad in UV (texture coordinate) space. 53 |

54 | 55 |

56 | Blender apply normal map 57 | Loading the normal map into Blender 2.77's texture image panel appears straightforward, there's even 58 | a mapping called "Norm" that sounds correct. But the default behavior here is to take the luminance 59 | of the incoming image and treat it as a height map, which is not what we want at all. So while this 60 | is a needed step, it is not enough on its own. 61 |

62 | 63 |

64 | Example of wrong behavior 65 | I didn't build this model just to test Blender, of course. There are several combinations of engines 66 | and shaders that appear, at least to my eye, to treat normal maps somewhat differently, or produce 67 | different results from them. The "twisting around" of the light source shown here is exactly what 68 | I'm trying to avoid (although there are additional problems in this image due to incorrect import 69 | settings in Blender). 70 |

71 | 72 |

73 | Blender import normal map 74 | Next let's fix the import settings. Here I've told Blender to treat the incoming image as non-color, 75 | and interpret it as a tangent-space normal map, which it is. 76 |

77 | 78 |

79 | Example of DirectX export 80 | Well, that's improved, but I still see the light source moving around when the UV map orientation 81 | changes, so it's no good. It turns out that Substance Painter 2.4 has texture export options for two different 82 | kinds of normal maps, "OpenGL" and "DirectX" (their terms) in the texture export configuration 83 | panel. The "PBR MetalRough" preset in this version defaults to the "DirectX" version, which has 84 | the effect of flipping the V axis vectors (the green channel intensity) compared to "OpenGL" version. 85 |

86 | This is not what Blender or Three.JS expects, and so produces the bad result shown here. 87 |

88 | 89 |

90 | Example of OpenGL export 91 | At last, the correct result was achieved by using Substance Painter's "OpenGL" export option 92 | paired with the Blender settings shown above. The same map also works in Three.js: 93 | View Demo. 94 |

95 | Notice that all of the tiles now appear very consistent when viewed directly face-on, with 96 | the light source coming from the same direction, regardless of the twisting happening in the 97 | texture coordinate space. 98 |

99 | 100 |

101 | I hope this test object proves useful, particularly to the development of glTF 2.0 and its own 102 | normal map implementations. The next step from here will be to construct a glTF 2.0 model that 103 | uses this map, and test its behavior in a variety of glTF loaders and engines. 104 |

105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /normal-tangent-test.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Normal-Tangent Test Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | README 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /normal-tangent-test.js: -------------------------------------------------------------------------------- 1 | init(); 2 | animate(); 3 | 4 | function init() { 5 | width = window.innerWidth; 6 | height = window.innerHeight; 7 | 8 | scene = new THREE.Scene(); 9 | 10 | var ambient = new THREE.AmbientLight( 0x101030 ); 11 | scene.add( ambient ); 12 | 13 | var directionalLight = new THREE.DirectionalLight(0xffeedd); 14 | directionalLight.position.set(0.5, 0.8, 1); 15 | scene.add(directionalLight); 16 | 17 | var fillLight = new THREE.DirectionalLight(0xddeeff); 18 | fillLight.position.set(-0.5, 0.2, -1); 19 | scene.add(fillLight); 20 | 21 | // Creative Commons license, downloaded from http://www.humus.name/index.php?page=Textures 22 | var envPath = "envmaps/Yokohama/"; 23 | var urls = [ 24 | envPath + 'posx.jpg', 25 | envPath + 'negx.jpg', 26 | envPath + 'posy.jpg', 27 | envPath + 'negy.jpg', 28 | envPath + 'posz.jpg', 29 | envPath + 'negz.jpg' 30 | ]; 31 | 32 | var reflectionCube = new THREE.CubeTextureLoader().load(urls); 33 | reflectionCube.format = THREE.RGBFormat; 34 | 35 | // This line displays the reflectionCube as the scene's background. 36 | //scene.background = reflectionCube; 37 | scene.background = new THREE.Color( 0.2, 0.2, 0.2 ); 38 | 39 | camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 200 ); 40 | camera.position.copy(new THREE.Vector3(0, 1.5, 0)); 41 | 42 | var manager = new THREE.LoadingManager(); 43 | manager.onProgress = function ( item, loaded, total ) { 44 | console.log( item, loaded, total ); 45 | }; 46 | 47 | var objLoader = new THREE.OBJLoader(manager); 48 | objLoader.load('models/NormalTangentTest/NormalTangentTestPlatform.obj', function (object) { 49 | var mainMesh; 50 | object.traverse(function (child) { 51 | if (child instanceof THREE.Mesh) { 52 | mainMesh = child; 53 | } 54 | }); 55 | 56 | var textureLoader = new THREE.TextureLoader(); 57 | mainMesh.material = new THREE.MeshStandardMaterial({ 58 | map: textureLoader.load('models/NormalTangentTest/NormalTangentTest_BaseColor.png'), 59 | normalMap: textureLoader.load('models/NormalTangentTest/NormalTangentTest_Normal.png'), 60 | roughnessMap: textureLoader.load('models/NormalTangentTest/NormalTangentTest_Roughness.png'), 61 | metalnessMap: textureLoader.load('models/NormalTangentTest/NormalTangentTest_Metallic.png'), 62 | aoMap: textureLoader.load('models/NormalTangentTest/NormalTangentTest_Occlusion.png'), 63 | 64 | //roughness: 0, 65 | //metalness: 1, 66 | aoMapIntensity: 0.5, // The ao map appears to quash the reflectionCube. 67 | envMap: reflectionCube 68 | }); 69 | 70 | object.position.set(0, -0.125, 0); 71 | scene.add(object); 72 | window.mainObject = object; // debugging 73 | window.mainMesh = mainMesh; // debugging 74 | }); 75 | 76 | renderer = new THREE.WebGLRenderer(); 77 | renderer.setClearColor( 0xaaaaaa ); 78 | 79 | controls = new THREE.OrbitControls( camera, renderer.domElement ); 80 | controls.userPan = false; 81 | controls.userPanSpeed = 0.0; 82 | controls.maxDistance = 5000.0; 83 | controls.maxPolarAngle = Math.PI * 0.895; 84 | controls.autoRotate = false; 85 | controls.autoRotateSpeed = -10.0; 86 | 87 | renderer.setSize( width, height ); 88 | document.body.appendChild( renderer.domElement ); 89 | } 90 | 91 | function animate() { 92 | requestAnimationFrame( animate ); 93 | renderer.render( scene, camera ); 94 | controls.update(); 95 | } 96 | -------------------------------------------------------------------------------- /screenshots/FireHydrant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/FireHydrant.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/BlenderApplyNormalMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/BlenderApplyNormalMap.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/BlenderNormalMapImportButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/BlenderNormalMapImportButton.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/ExampleOfCorrectNormalMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/ExampleOfCorrectNormalMap.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/ExampleOfDirectXNormalsUsedInBlender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/ExampleOfDirectXNormalsUsedInBlender.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/ExampleOfUVDirections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/ExampleOfUVDirections.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/ExampleOfWrongBehavior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/ExampleOfWrongBehavior.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/OffAxis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/OffAxis.png -------------------------------------------------------------------------------- /screenshots/NormalTangentTest/UV_Positions_Arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emackey/testing-pbr/6afba39ddf5631b6cbf08dd031b0edd541b1e7eb/screenshots/NormalTangentTest/UV_Positions_Arrows.png -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | background: #000; 10 | font: 30px sans-serif; 11 | } 12 | 13 | #container { 14 | position: absolute; 15 | top: 0px; 16 | width:100%; 17 | height:100%; 18 | z-index: -1; 19 | } 20 | 21 | #rendercontainer { 22 | position: absolute; 23 | top: 0px; 24 | width:100%; 25 | height:100%; 26 | z-index: -1; 27 | background-color:Red; 28 | } 29 | 30 | a.readme, 31 | a.readme:visited { 32 | color: white; 33 | display: block; 34 | position: absolute; 35 | top: 10px; 36 | left: 15px; 37 | text-decoration: none; 38 | font: 12px sans-serif; 39 | } 40 | 41 | a.readme:hover { 42 | text-decoration: underline; 43 | } 44 | --------------------------------------------------------------------------------