Mr.doob 6 years ago
parent
commit
f15a0e4e03
100 changed files with 6530 additions and 2940 deletions
  1. 326 625
      build/three.js
  2. 381 379
      build/three.min.js
  3. 326 625
      build/three.module.js
  4. 2 0
      docs/api/en/core/Object3D.html
  5. 4 5
      docs/api/en/loaders/AnimationLoader.html
  6. 10 0
      docs/api/en/loaders/ImageBitmapLoader.html
  7. 1 1
      docs/api/en/loaders/ImageLoader.html
  8. 24 0
      docs/api/en/loaders/Loader.html
  9. 1 1
      docs/api/en/materials/Material.html
  10. 1 1
      docs/api/en/math/Matrix3.html
  11. 1 1
      docs/api/en/math/Matrix4.html
  12. 7 0
      docs/api/en/math/Quaternion.html
  13. 1 1
      docs/api/en/objects/Mesh.html
  14. 0 27
      docs/api/en/renderers/WebGLRenderer.html
  15. 7 0
      docs/api/en/textures/Texture.html
  16. 4 7
      docs/api/zh/loaders/AnimationLoader.html
  17. 10 0
      docs/api/zh/loaders/ImageBitmapLoader.html
  18. 20 0
      docs/api/zh/loaders/Loader.html
  19. 1 1
      docs/api/zh/materials/Material.html
  20. 3 3
      docs/api/zh/math/Matrix3.html
  21. 5 5
      docs/api/zh/math/Matrix4.html
  22. 8 1
      docs/api/zh/math/Quaternion.html
  23. 1 1
      docs/api/zh/objects/Mesh.html
  24. 0 23
      docs/api/zh/renderers/WebGLRenderer.html
  25. 8 0
      docs/api/zh/textures/Texture.html
  26. 1 0
      docs/examples/exporters/GLTFExporter.html
  27. 145 0
      docs/examples/loaders/DRACOLoader.html
  28. 3 0
      docs/examples/loaders/GLTFLoader.html
  29. 1 1
      docs/examples/loaders/OBJLoader2.html
  30. 2 1
      docs/examples/loaders/SVGLoader.html
  31. 2 3
      docs/examples/objects/Lensflare.html
  32. 60 0
      docs/examples/utils/SkeletonUtils.html
  33. 3 1
      docs/list.js
  34. 1 1
      editor/css/dark.css
  35. 1 1
      editor/css/light.css
  36. 3 11
      editor/index.html
  37. 47 6
      editor/js/Editor.js
  38. 1 1
      editor/js/Loader.js
  39. 1 1
      editor/js/Menubar.Add.js
  40. 1 1
      editor/js/Sidebar.Settings.js
  41. 48 0
      editor/js/Viewport.Camera.js
  42. 30 11
      editor/js/Viewport.js
  43. 51 58
      editor/js/libs/ui.js
  44. 1 1
      editor/sw.js
  45. 50 39
      examples/files.js
  46. 4 2
      examples/js/MD2Character.js
  47. 4 2
      examples/js/MD2CharacterComplex.js
  48. 73 0
      examples/js/MarchingCubes.js
  49. 14 14
      examples/js/controls/EditorControls.js
  50. 13 7
      examples/js/controls/TransformControls.js
  51. 206 0
      examples/js/exporters/DracoExporter.js
  52. 62 29
      examples/js/exporters/GLTFExporter.js
  53. 3 0
      examples/js/libs/draco/draco_encoder.js
  54. 2 2
      examples/js/loaders/GCodeLoader.js
  55. 183 403
      examples/js/loaders/GLTFLoader.js
  56. 2310 0
      examples/js/loaders/LWOLoader.js
  57. 883 27
      examples/js/loaders/SVGLoader.js
  58. 1 4
      examples/js/loaders/ctm/CTMLoader.js
  59. 1 2
      examples/js/loaders/sea3d/SEA3D.js
  60. 1 1
      examples/js/nodes/core/StructNode.js
  61. 26 0
      examples/js/nodes/materials/NodeMaterial.js
  62. 1 1
      examples/js/objects/Fire.js
  63. 32 34
      examples/js/objects/LightningStorm.js
  64. 1 2
      examples/js/objects/Sky.js
  65. 1 0
      examples/js/offscreen/scene.js
  66. 11 16
      examples/js/postprocessing/AdaptiveToneMappingPass.js
  67. 20 23
      examples/js/postprocessing/AfterimagePass.js
  68. 6 11
      examples/js/postprocessing/BloomPass.js
  69. 3 10
      examples/js/postprocessing/BokehPass.js
  70. 3 10
      examples/js/postprocessing/DotScreenPass.js
  71. 62 1
      examples/js/postprocessing/EffectComposer.js
  72. 3 10
      examples/js/postprocessing/FilmPass.js
  73. 3 9
      examples/js/postprocessing/GlitchPass.js
  74. 3 8
      examples/js/postprocessing/HalftonePass.js
  75. 15 20
      examples/js/postprocessing/OutlinePass.js
  76. 3 6
      examples/js/postprocessing/SAOPass.js
  77. 8 13
      examples/js/postprocessing/SMAAPass.js
  78. 2 6
      examples/js/postprocessing/SSAARenderPass.js
  79. 3 10
      examples/js/postprocessing/SSAOPass.js
  80. 2 9
      examples/js/postprocessing/SavePass.js
  81. 4 10
      examples/js/postprocessing/ShaderPass.js
  82. 3 3
      examples/js/postprocessing/TAARenderPass.js
  83. 3 8
      examples/js/postprocessing/TexturePass.js
  84. 13 18
      examples/js/postprocessing/UnrealBloomPass.js
  85. 2 1
      examples/js/renderers/CSS2DRenderer.js
  86. 3 3
      examples/js/renderers/Projector.js
  87. 4 37
      examples/js/renderers/WebGLDeferredRenderer.js
  88. 2 1
      examples/js/utils/BufferGeometryUtils.js
  89. 52 0
      examples/js/utils/SkeletonUtils.js
  90. 0 0
      examples/js/utils/TypedArrayUtils.js
  91. 2 2
      examples/js/utils/UVsDebug.js
  92. 0 300
      examples/js/utils/ldraw/packLDrawModel.js
  93. 71 0
      examples/jsm/controls/MapControls.d.ts
  94. 8 8
      examples/jsm/controls/MapControls.js
  95. 70 0
      examples/jsm/controls/OrbitControls.d.ts
  96. 31 12
      examples/jsm/controls/OrbitControls.js
  97. 47 0
      examples/jsm/controls/TrackballControls.d.ts
  98. 1 1
      examples/jsm/controls/TrackballControls.js
  99. 635 0
      examples/jsm/exporters/ColladaExporter.js
  100. 7 0
      examples/jsm/exporters/GLTFExporter.d.ts

File diff suppressed because it is too large
+ 326 - 625
build/three.js


File diff suppressed because it is too large
+ 381 - 379
build/three.min.js


File diff suppressed because it is too large
+ 326 - 625
build/three.module.js


+ 2 - 0
docs/api/en/core/Object3D.html

@@ -229,6 +229,8 @@
 		recursive -- if true, descendants of the object are also copied. Default is true.<br /><br />
 
 		Copy the given object into this object.
+
+		Note: event listeners and user-defined callbacks ([page:.onAfterRender] and [page:.onBeforeRender]) are not copied.
 		</p>
 
 		<h3>[method:Object3D getObjectById]( [param:Integer id] )</h3>

+ 4 - 5
docs/api/en/loaders/AnimationLoader.html

@@ -65,19 +65,18 @@
 		<p>
 		[page:String url] — the path or URL to the file. This can also be a
 			[link:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs Data URI].<br />
-		[page:Function onLoad] — Will be called when load completes. The argument will be the loaded [page:Animation animation].<br />
+		[page:Function onLoad] — Will be called when load completes. The argument will be the loaded [page:AnimationClip animation clips].<br />
 		[page:Function onProgress] — Will be called while load progresses. The argument will be the XMLHttpRequest instance, which contains .[page:Integer total] and .[page:Integer loaded] bytes.<br />
 		[page:Function onError] — Will be called if load errors.<br /><br />
 
 		Begin loading from url and pass the loaded animation to onLoad.
 		</p>
 
-		<h3>[method:null parse]( [param:JSON json], [param:Function onLoad] )</h3>
+		<h3>[method:Array parse]( [param:JSON json] )</h3>
 		<p>
-		[page:JSON json] — required<br />
-		[page:Function onLoad] — Will be called when parsing completes. <br /><br />
+		[page:JSON json] — required<br /><br />
 
-		Parse the JSON object and pass the result to onLoad. Individual clips in the object will
+		Parse the JSON object and return an array of animation clips. Individual clips in the object will
 		be parsed with [page:AnimationClip.parse].
 		</p>
 

+ 10 - 0
docs/api/en/loaders/ImageBitmapLoader.html

@@ -16,6 +16,13 @@
 			Unlike [page:FileLoader], [name] does not avoid multiple concurrent requests to the same URL.
 		</p>
 
+		<p>
+			Note that [page:Texture.flipY] and [page:Texture.premultiplyAlpha] with [link:https://developer.mozilla.org/de/docs/Web/API/ImageBitmap ImageBitmap] are ignored.
+			[link:https://developer.mozilla.org/de/docs/Web/API/ImageBitmap ImageBitmap] needs these configuration on bitmap creation
+			unlike regular images need them on uploading to GPU. You need to set the equivalent options via [page:ImageBitmapLoader.setOptions]
+			instead. Refer to [link:https://www.khronos.org/registry/webgl/specs/latest/1.0/#6.10 WebGL specification] for the detail.
+		</p>
+
 		<h2>Example</h2>
 
 		<p>
@@ -26,6 +33,9 @@
 		// instantiate a loader
 		var loader = new THREE.ImageBitmapLoader();
 
+		// set options if needed
+		loader.setOptions( { imageOrientation: 'flipY' } );
+
 		// load a image resource
 		loader.load(
 			// resource URL

+ 1 - 1
docs/api/en/loaders/ImageLoader.html

@@ -12,7 +12,7 @@
 
 		<p class="desc">
 			A loader for loading an [page:Image].
-			This uses the [page:FileLoader] internally for loading files, and is used internally by the
+			This is used internally by the
 			[page:CubeTextureLoader], [page:ObjectLoader] and [page:TextureLoader].
 		</p>
 

+ 24 - 0
docs/api/en/loaders/Loader.html

@@ -62,6 +62,30 @@
 		Creates an array of [page:Material] based on the array of parameters m. The index of the parameters decide the correct index of the materials.
 		</p>
 
+		<h2>Handlers</h2>
+
+		<p>
+		*[name].Handlers* is a special object normally used by other loaders like [page:GLTFLoader] or [page:MTLLoader]. It provides an
+		API that allows the definition of special mappings: What loaders should be used in order to load specific files. A typical use case
+		is to overwrite the default loader for textures.<br /><br />
+
+		Note: It's only possible to use *[name].Handlers* if the respective loader support the usage.
+		</p>
+
+		<h3>[method:null add]( [param:Object regex], [param:Loader loader] )</h3>
+		<p>
+		[page:Object regex] — A regular expression.<br />
+		[page:Loader loader] — The loader.
+		<p>
+		Registers a loader with the given regular expression.
+		</p>
+
+		<h3>[method:null get]( [param:String file] )</h3>
+		<p>
+		[page:String file] — The file path.
+		<p>
+		Can be used to retrieve the registered loader for the given file path.
+		</p>
 
 		<h2>Source</h2>
 

+ 1 - 1
docs/api/en/materials/Material.html

@@ -295,7 +295,7 @@
 		These needs to be disposed by [page:Texture Texture].
 		</p>
 
-		<h3>[method:null onBeforeCompile]( [param:Object shader], [param:WebGLRenderer renderer] )</h3>
+		<h3>[method:null onBeforeCompile]( [param:Shader shader], [param:WebGLRenderer renderer] )</h3>
 		<p>
 		An optional callback that is executed immediately before the shader program is compiled.
 		This function is called with the shader source code as a parameter. Useful for the modification of built-in materials.

+ 1 - 1
docs/api/en/math/Matrix3.html

@@ -55,7 +55,7 @@ m.elements = [ 11, 21, 31,
 
 		<h2>Properties</h2>
 
-		<h3>[property:Float32Array elements]</h3>
+		<h3>[property:Array elements]</h3>
 		<p>
 		A [link:https://en.wikipedia.org/wiki/Row-_and_column-major_order column-major]
 		 list of matrix values.

+ 1 - 1
docs/api/en/math/Matrix4.html

@@ -90,7 +90,7 @@ m.elements = [ 11, 21, 31, 41,
 
 		<h2>Properties</h2>
 
-		<h3>[property:Float32Array elements]</h3>
+		<h3>[property:Array elements]</h3>
 		<p>
 		A [link:https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order column-major]
 		 list of matrix values.

+ 7 - 0
docs/api/en/math/Quaternion.html

@@ -44,6 +44,13 @@
 
 		<h2>Properties</h2>
 
+		<h3>[property:Boolean isQuaternion]</h3>
+		<p>
+			Used to check whether this or derived classes are Quaternions. Default is *true*.<br /><br />
+
+			You should not change this, as it is used internally for optimisation.
+		</p>
+
 		<h3>[property:Float x]</h3>
 		<p>Changing this property will result in [page:.onChangeCallback onChangeCallback] being called.</p>
 

+ 1 - 1
docs/api/en/objects/Mesh.html

@@ -95,7 +95,7 @@
 		<h3>[method:null raycast]( [param:Raycaster raycaster], [param:Array intersects] )</h3>
 		<p>
 		Get intersections between a casted ray and this mesh.
-		[page:Raycaster.intersectObject] will call this method.
+		[page:Raycaster.intersectObject] will call this method, but the results are not ordered.
 		</p>
 
 		<h3>[method:null updateMorphTargets]()</h3>

+ 0 - 27
docs/api/en/renderers/WebGLRenderer.html

@@ -279,13 +279,6 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:Integer allocTextureUnit]</h3>
-		<p>
-		Attempt to allocate a texture unit for use by a shader. Will warn if trying to allocate
-		more texture units than the GPU supports. This is mainly used internally.
-		See [page:WebGLRenderer.capabilities capabilities.maxTextures].
-		</p>
-
 		<h3>[method:null clear]( [param:Boolean color], [param:Boolean depth], [param:Boolean stencil] )</h3>
 		<p>
 		Tells the renderer to clear its color, depth or stencil drawing buffer(s).
@@ -466,26 +459,6 @@
 			Setting [page:Boolean updateStyle] to false prevents any style changes to the output canvas.
 		</p>
 
-		<h3>[method:null setTexture2D]( [param:Texture texture], [param:number slot] )</h3>
-		<p>
-		texture -- The [page:Texture texture] that needs to be set.<br />
-		slot -- The number indicating which slot should be used by the texture.<br /><br />
-
-		This method sets the correct texture to the correct slot for the WebGL shader.
-		The slot number can be found as a value of the uniform of the sampler.<br /><br />
-
-		Note: This method replaces the older [method:null setTexture] method.
-		</p>
-
-		<h3>[method:null setTextureCube]( [param:CubeTexture cubeTexture], [param:Number slot] )</h3>
-		<p>
-		texture -- The [page:CubeTexture cubeTexture] that needs to be set.<br />
-		slot -- The number indicating which slot should be used by the texture.<br /><br />
-
-		This method sets the correct texture to the correct slot for the WebGL shader.
-		The slot number can be found as a value of the uniform of the sampler.
-		</p>
-
 		<h3>[method:null setViewport]( [param:Integer x], [param:Integer y], [param:Integer width], [param:Integer height] )<br />
 		[method:null setViewport]( [param:Vector4 vector] )</h3>
 

+ 7 - 0
docs/api/en/textures/Texture.html

@@ -55,6 +55,13 @@
 		as long as video is playing - the [page:VideoTexture VideoTexture] class handles this automatically.
 		</p>
 
+		<h3>[property:Boolean isTexture]</h3>
+		<p>
+			Used to check whether this or derived classes are Textures. Default is *true*.<br /><br />
+
+			You should not change this, as it is used internally for optimisation.
+		</p>
+
 		<h3>[property:array mipmaps]</h3>
 		<p>
 		Array of user-specified mipmaps (optional).

+ 4 - 7
docs/api/zh/loaders/AnimationLoader.html

@@ -65,20 +65,17 @@
 		<p>
 		[page:String url] — 文件的URL或者路径,也可以为
 			[link:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs Data URI].<br />
-		[page:Function onLoad] — 加载完成时将调用。回调参数为将要加载的[page:Animation animation].<br />
+		[page:Function onLoad] — 加载完成时将调用。回调参数为将要加载的[page:AnimationClip animation clips].<br />
 		[page:Function onProgress] —  将在加载过程中进行调用。参数为XMLHttpRequest实例,实例包含[page:Integer total]和[page:Integer loaded]字节。<br />
 		[page:Function onError] — 在加载错误时被调用。<br /><br />
 
             从URL中进行加载并将动画传递给onLoad。
 		</p>
 
-		<h3>[method:null parse]( [param:JSON json], [param:Function onLoad] )</h3>
+		<h3>[method:Array parse]( [param:JSON json] )</h3>
 		<p>
-		[page:JSON json] — 请求<br />
-		[page:Function onLoad] — 当解析完成时,将被调用 <br /><br />
-
-            解析JSON对象并将结果传递给onLoad。对象中的单个片段将
-            用[page [page:AnimationClip.parse]进行解析。
+		[page:JSON json] — 请求 <br /><br />
+			TODO
 		</p>
 
 		<h2>源</h2>

+ 10 - 0
docs/api/zh/loaders/ImageBitmapLoader.html

@@ -16,6 +16,13 @@
 			不像[page:FileLoader], [name]无需避免对同一的URL进行多次请求。
 		</p>
 
+		<p>
+			Note that [page:Texture.flipY] and [page:Texture.premultiplyAlpha] with [link:https://developer.mozilla.org/de/docs/Web/API/ImageBitmap ImageBitmap] are ignored.
+			[link:https://developer.mozilla.org/de/docs/Web/API/ImageBitmap ImageBitmap] needs these configuration on bitmap creation
+			unlike regular images need them on uploading to GPU. You need to set the equivalent options via [page:ImageBitmapLoader.setOptions]
+			instead. Refer to [link:https://www.khronos.org/registry/webgl/specs/latest/1.0/#6.10 WebGL specification] for the detail.
+		</p>
+
 		<h2>例子</h2>
 
 		<p>
@@ -26,6 +33,9 @@
 		// 初始化一个加载器
 		var loader = new THREE.ImageBitmapLoader();
 
+		// set options if needed
+		loader.setOptions( { imageOrientation: 'flipY' } );
+
 		// 加载一个图片资源
 		loader.load(
 			// 资源的URL

+ 20 - 0
docs/api/zh/loaders/Loader.html

@@ -61,6 +61,26 @@
 		基于参数数组m,来创建 [page:Material] 数组. 参数索引与材质的索引一致。
 		</p>
 
+		<h2>Handlers</h2>
+
+		<p>
+		TODO
+		</p>
+
+		<h3>[method:null add]( [param:Object regex], [param:Loader loader] )</h3>
+		<p>
+		[page:Object regex] — TODO<br />
+		[page:Loader loader] — TODO
+		<p>
+		TODO
+		</p>
+
+		<h3>[method:null get]( [param:String file] )</h3>
+		<p>
+		[page:String file] — TODO
+		<p>
+		TODO
+		</p>
 
 		<h2>Source</h2>
 

+ 1 - 1
docs/api/zh/materials/Material.html

@@ -243,7 +243,7 @@
 <p> 处理材质。材质的纹理不会被处理。需要通过[page:Texture Texture]处理。
 </p>
 
-<h3>[method:null onBeforeCompile]( [param:Object shader], [param:WebGLRenderer renderer] )</h3>
+<h3>[method:null onBeforeCompile]( [param:Shader shader], [param:WebGLRenderer renderer] )</h3>
 <p> 在编译shader程序之前立即执行的可选回调。此函数使用shader源码作为参数。用于修改内置材质。
 </p>
 

+ 3 - 3
docs/api/zh/math/Matrix3.html

@@ -53,7 +53,7 @@ m.elements = [ 11, 21, 31,
 
 		<h2>属性(Properties)</h2>
 
-		<h3>[property:Float32Array elements]</h3>
+		<h3>[property:Array elements]</h3>
 		<p>
 			矩阵列优先[link:https://en.wikipedia.org/wiki/Row-_and_column-major_order column-major]列表。
 		</p>
@@ -61,7 +61,7 @@ m.elements = [ 11, 21, 31,
 		<h3>[property:Boolean isMatrix3]</h3>
 		<p>
 				用于判定此对象或者此类的派生对象是否是三维矩阵。默认值为 *true*。<br /><br />
-			
+
 				不应该改变该值,因为它在内部用于优化。
 		</p>
 
@@ -103,7 +103,7 @@ m.elements = [ 11, 21, 31,
 		<p>
 		[page:Matrix3 m] - 取逆的矩阵。<br />
 		[page:Boolean throwOnDegenerate] - (optional) 如果设置为true,如果矩阵是退化的(如果不可逆的话),则会抛出一个错误。<br /><br />
-		
+
 		使用逆矩阵计算方法[link:https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution analytic method],
 		将当前矩阵设置为给定矩阵的逆矩阵[link:https://en.wikipedia.org/wiki/Invertible_matrix inverse],如果[page:Boolean throwOnDegenerate]
 		参数没有设置且给定矩阵不可逆,那么将当前矩阵设置为3X3单位矩阵。

+ 5 - 5
docs/api/zh/math/Matrix4.html

@@ -83,7 +83,7 @@ m.elements = [ 11, 21, 31, 41,
 
 		<h2>属性(Properties)</h2>
 
-		<h3>[property:Float32Array elements]</h3>
+		<h3>[property:Array elements]</h3>
 		<p>
 		矩阵列优先[link:https://en.wikipedia.org/wiki/Row-_and_column-major_order column-major]列表。
 		</p>
@@ -91,7 +91,7 @@ m.elements = [ 11, 21, 31, 41,
 		<h3>[property:Boolean isMatrix4]</h3>
 		<p>
 			用于判定此对象或者此类的派生对象是否是三维矩阵。默认值为 *true*。<br /><br />
-		
+
 			不应该改变该值,因为它在内部用于优化。
 		</p>
 
@@ -168,7 +168,7 @@ zAxis = (c, g, k)
 		<p>
 			[page:Array array] - 用来存储设置元素数据的数组<br />
 			[page:Integer offset] - (可选参数) 数组的偏移量,默认值为 0。<br /><br />
-	
+
 			使用基于列优先格式[link:https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order column-major]的数组来设置该矩阵。
 			</p>
 
@@ -176,7 +176,7 @@ zAxis = (c, g, k)
 		<p>
 			[page:Matrix3 m] - 取逆的矩阵。<br />
 			[page:Boolean throwOnDegenerate] - (optional) 如果设置为true,如果矩阵是退化的(如果不可逆的话),则会抛出一个错误。<br /><br />
-			
+
 			使用逆矩阵计算方法[link:https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution analytic method],
 			将当前矩阵设置为给定矩阵的逆矩阵[link:https://en.wikipedia.org/wiki/Invertible_matrix inverse],如果[page:Boolean throwOnDegenerate]
 			参数没有设置且给定矩阵不可逆,那么将当前矩阵设置为3X3单位矩阵。
@@ -251,7 +251,7 @@ xAxis.z, yAxis.z, zAxis.z, 0,
 		<h3>[method:this makeRotationX]( [param:Float theta] )</h3>
 		<p>
 		[page:Float theta] — Rotation angle in radians.<br /><br />
-		
+
 		把该矩阵设置为绕x轴旋转弧度[page:Float theta] (&theta;)大小的矩阵。
 		结果如下:
 		<code>

+ 8 - 1
docs/api/zh/math/Quaternion.html

@@ -44,6 +44,13 @@
 
 		<h2>Properties</h2>
 
+		<h3>[property:Boolean isQuaternion]</h3>
+		<p>
+			Used to check whether this or derived classes are Quaternions. Default is *true*.<br /><br />
+
+			You should not change this, as it is used internally for optimisation.
+		</p>
+
 		<h3>[property:Float x]</h3>
 		<p>Changing this property will result in [page:.onChangeCallback onChangeCallback] being called.</p>
 
@@ -290,4 +297,4 @@ q.slerp( qb, t )
 
 		[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
 	</body>
-</html>
+</html>

+ 1 - 1
docs/api/zh/objects/Mesh.html

@@ -91,7 +91,7 @@
 		<h3>[method:null raycast]( [param:Raycaster raycaster], [param:Array intersects] )</h3>
 		<p>
 			在一条投射出去的[page:Ray](射线)和这个网格之间产生交互。
-			[page:Raycaster.intersectObject]将会调用这个方法
+			[page:Raycaster.intersectObject]将会调用这个方法但是这个结果是未排序的
 		</p>
 
 		<h3>[method:null updateMorphTargets]()</h3>

+ 0 - 23
docs/api/zh/renderers/WebGLRenderer.html

@@ -254,12 +254,6 @@
 
 		<h2>方法</h2>
 
-		<h3>[method:Integer allocTextureUnit]</h3>
-		<p>
-		尝试分配纹理单元以供着色器使用。如果尝试分配超过GPU支持量的纹理单元,则会报警告。主要供内部使用。
-		请参阅[page:WebGLRenderer.capabilities capabilities.maxTextures]。
-		</p>
-
 		<h3>[method:null clear]( [param:Boolean color], [param:Boolean depth], [param:Boolean stencil] )</h3>
 		<p>
 		告诉渲染器清除颜色、深度或模板缓存.
@@ -412,23 +406,6 @@
 		将[page:Boolean updateStyle]设置为false以阻止对canvas的样式做任何改变。
 		</p>
 
-		<h3>[method:null setTexture2D]( [param:Texture texture], [param:number slot] )</h3>
-		<p>
-		texture -- 需被设置的[page:Texture texture]<br />
-		slot -- 纹理应该使用的插槽号<br /><br />
-        该方法为WebGL着色器将正确的纹理设置到正确的插槽中。插槽号可作为取样器的全局变量(uniform)<br /><br />
-
-		说明: 该方法取代了旧的[method:null setTexture]方法
-		</p>
-
-		<h3>[method:null setTextureCube]( [param:CubeTexture cubeTexture], [param:Number slot] )</h3>
-		<p>
-		texture -- 需要被设置的[page:CubeTexture cubeTexture]<br />
-		slot -- 纹理应该使用的插槽号<br /><br />
-
-        该方法为WebGL着色器将正确的纹理设置到正确的插槽中。插槽号可作为取样器的全局变量(uniform)
-		</p>
-
 		<h3>[method:null setViewport]( [param:Integer x], [param:Integer y], [param:Integer width], [param:Integer height] )</h3>
 		<p>将视口大小设置为(x, y)到 (x + width, y + height).</p>
 

+ 8 - 0
docs/api/zh/textures/Texture.html

@@ -54,6 +54,14 @@
 		并在视频播放时不断地更新这个纹理贴图。——[page:VideoTexture VideoTexture] 类会对此自动进行处理。
 		</p>
 
+		<h3>[property:Boolean isTexture]</h3>
+		<p>
+			用于测试这个类或者派生类是否为Texture,默认为*true*。<br /><br />
+
+			你不应当对这个属性进行改变,因为它在内部使用,以用于优化。
+		</p>
+
+
 		<h3>[property:array mipmaps]</h3>
 		<p>
 		用户所给定的mipmap数组(可选)。

+ 1 - 0
docs/examples/exporters/GLTFExporter.html

@@ -97,6 +97,7 @@
 			<li>animations - Array<[page:AnimationClip AnimationClip]>. List of animations to be included in the export.</li>
 			<li>forceIndices - bool. Generate indices for non-index geometry and export with them. Default is false.</li>
 			<li>forcePowerOfTwoTextures - bool. Export with images resized to POT size. This option works only if embedImages is true. Default is false.</li>
+			<li>includeCustomExtensions - bool. Export custom glTF extensions defined on an object's <em>userData.gltfExtensions</em> property. Default is false.</li>
 		</ul>
 		</p>
 		<p>

+ 145 - 0
docs/examples/loaders/DRACOLoader.html

@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../" />
+		<script src="list.js"></script>
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Loader] &rarr;
+		<h1>[name]</h1>
+
+		<p class="desc">
+			A loader for geometry compressed with the Draco library. <br /><br />
+			[link:https://google.github.io/draco/ Draco] is an open source library for compressing and
+			decompressing 3D meshes and point clouds. Compressed geometry can be significantly smaller,
+			at the cost of additional decoding time on the client device.
+		</p>
+
+		<p>
+			Standalone Draco files have a <em>.drc</em> extension, and contain vertex positions,
+			normals, colors, and other attributes. Draco files <em>do not</em> contain materials,
+			textures, animation, or node hierarchies – to use these features, embed Draco geometry
+			inside of a glTF file. A normal glTF file can be converted to a Draco-compressed glTF file
+			using [link:https://github.com/AnalyticalGraphicsInc/gltf-pipeline glTF-Pipeline]. When
+			using Draco with glTF, an instance of DRACOLoader will be used internally by [page:GLTFLoader].
+		</p>
+
+		<h2>Example</h2>
+
+		<code>
+		// Instantiate a loader
+		var loader = new THREE.DRACOLoader();
+
+		// Specify path to a folder containing WASM/JS decoding libraries.
+		THREE.DRACOLoader.setDecoderPath( '/examples/js/libs/draco' );
+
+		// Optional: Pre-fetch Draco WASM/JS module.
+		THREE.DRACOLoader.getDecoderModule();
+
+		// Load a Draco geometry
+		loader.load(
+			// resource URL
+			'model.drc',
+			// called when the resource is loaded
+			function ( geometry ) {
+
+				var material = new THREE.MeshStandardMaterial( { color: 0x606060 } );
+				var mesh = new THREE.Mesh( geometry, material );
+				scene.add( mesh );
+
+			},
+			// called as loading progresses
+			function ( xhr ) {
+
+				console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
+
+			},
+			// called when loading has errors
+			function ( error ) {
+
+				console.log( 'An error happened' );
+
+			}
+		);
+		</code>
+
+		[example:webgl_loader_draco]
+
+		<h2>Browser compatibility</h2>
+
+		<p>DRACOLoader relies on ES6 [link:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promises],
+		which are not supported in IE11. To use the loader in IE11, you must
+		[link:https://github.com/stefanpenner/es6-promise include a polyfill]
+		providing a Promise replacement. DRACOLoader will automatically use
+		either the JS or the WASM decoding library, based on browser
+		capabilities.</p>
+
+		<br>
+		<hr>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]( [param:LoadingManager manager] )</h3>
+		<p>
+		[page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager].
+		</p>
+		<p>
+		Creates a new [name].
+		</p>
+
+		<h2>Static Methods</h2>
+
+		<h3>[method:null setDecoderPath]( [param:String value] )</h3>
+		<p>
+		[page:String value] — Path to folder containing the JS and WASM decoder libraries.
+		</p>
+
+		<h3>[method:null setDecoderConfig]( [param:Object config] )</h3>
+		<p>
+			[page:String config.type] - (Optional) <em>"js"</em> or <em>"wasm"</em>.<br />
+		</p>
+		<p>
+		Provides configuration for the decoder libraries. Configuration cannot be changed
+		after loading the decoders.
+		</p>
+
+		<h3>[method:Promise getDecoderModule]()</h3>
+		<p>
+		Requests the decoder libraries, if not already loaded.
+		</p>
+
+		<h3>[method:null releaseDecoderModule]()</h3>
+		<p>
+		Disposes of the decoder library and deallocates memory. The decoder
+		[link:https://github.com/google/draco/issues/349 cannot be reloaded afterward].
+		</p>
+
+		<h2>Methods</h2>
+
+		<h3>[method:null load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )</h3>
+		<p>
+		[page:String url] — A string containing the path/URL of the <em>.drc</em> file.<br />
+		[page:Function onLoad] — A function to be called after the loading is successfully completed.<br />
+		[page:Function onProgress] — (optional) A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, that contains .[page:Integer total] and .[page:Integer loaded] bytes.<br />
+		[page:Function onError] — (optional) A function to be called if an error occurs during loading. The function receives error as an argument.<br />
+		</p>
+		<p>
+		Begin loading from url and call the <em>onLoad</em> function with the decompressed geometry.
+		</p>
+
+		<h3>[method:DRACOLoader setPath]( [param:String path] )</h3>
+		<p>
+		[page:String path] — Base path.
+		</p>
+		<p>
+		Set the base path for the <em>.drc</em> file.
+		</p>
+
+		<h2>Source</h2>
+
+		[link:https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/DRACOLoader.js examples/js/loaders/DRACOLoader.js]
+	</body>
+</html>

+ 3 - 0
docs/examples/loaders/GLTFLoader.html

@@ -58,6 +58,9 @@
 		// Optional: Provide a DRACOLoader instance to decode compressed mesh data
 		THREE.DRACOLoader.setDecoderPath( '/examples/js/libs/draco' );
 		loader.setDRACOLoader( new THREE.DRACOLoader() );
+			
+		// Optional: Pre-fetch Draco WASM/JS module, to save time while parsing.
+		THREE.DRACOLoader.getDecoderModule();
 
 		// Load a glTF resource
 		loader.load(

+ 1 - 1
docs/examples/loaders/OBJLoader2.html

@@ -52,7 +52,7 @@
 
 		<h2>Methods</h2>
 
-		<h3>[method:Object3D parse]( {[param:arraybuffer content]|[param:String content]] )</h3>
+		<h3>[method:Object3D parse]( [param:arraybuffer content]|[param:String content] )</h3>
 		<p>
 			[[page:arraybuffer content]|[page:String content]] OBJ data as Uint8Array or String
 		</p>

+ 2 - 1
docs/examples/loaders/SVGLoader.html

@@ -26,8 +26,9 @@
 			// resource URL
 			'data/svgSample.svg',
 			// called when the resource is loaded
-			function ( paths ) {
+			function ( data ) {
 
+				var paths = data.paths;
 				var group = new THREE.Group();
 
 				for ( var i = 0; i < paths.length; i ++ ) {

+ 2 - 3
docs/examples/objects/Lensflare.html

@@ -44,13 +44,12 @@ light.add( lensflare );
 		<h2>Constructor</h2>
 
 
-		<h3>LensflareElement( [param:Texture texture], [param:Float size], [param:Float distance], [param:Color color], [param:Materials blending] )</h3>
+		<h3>LensflareElement( [param:Texture texture], [param:Float size], [param:Float distance], [param:Color color] )</h3>
 		<p>
 		[page:Texture texture] - THREE.Texture to use for the flare. <br />
 		[page:Float size] - (optional) size in pixels <br />
 		[page:Float distance] - (optional) (0-1) from light source (0 = at light source) <br />
-		[page:Color color] - (optional) the [page:Color] of the lens flare<br />
-		[page:Materials blending] - (optional) [page:Materials Blending Mode] - Defaults to THREE.NormalBlending
+		[page:Color color] - (optional) the [page:Color] of the lens flare
 		</p>
 
 		<h2>Properties</h2>

+ 60 - 0
docs/examples/utils/SkeletonUtils.html

@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../" />
+		<script src="list.js"></script>
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		<h1>[name]</h1>
+
+		<p class="desc">Utility functions for [page:Skeleton], [page:SkinnedMesh], and [page:Bone] manipulation.</p>
+
+
+		<h2>Methods</h2>
+
+		<h3>[method:Object3D clone]( [param:Object3D object] )</h3>
+		<p>
+			Clones the given object and its descendants, ensuring that any [page:SkinnedMesh] instances
+			are correctly associated with their bones. Bones are also cloned, and must be descendants of
+			the object passed to this method. Other data, like geometries and materials, are reused by
+			reference.
+		</p>
+
+		<h3>[method:Object findBoneTrackData]( [param:String name], [param:Array tracks] )</h3>
+		<p></p>
+
+		<h3>[method:Bone getBoneByName]( [param:String name], [param:Skeleton skeleton] )</h3>
+		<p></p>
+
+		<h3>[method:Array getBones]( [param:Skeleton skeleton] )</h3>
+		<p></p>
+
+		<h3>[method:Array getEqualsBonesNames]( [param:Skeleton skeleton], [param:Skeleton targetSkeleton] )</h3>
+		<p></p>
+
+		<h3>[method:SkeletonHelper getHelperFromSkeleton]( [param:Skeleton skeleton] )</h3>
+		<p></p>
+
+		<h3>[method:Bone getNearestBone]( [param:Bone bone], [param:Array names] )</h3>
+		<p></p>
+
+		<h3>[method:Object getSkeletonOffsets]( [param:SkeletonHelper target], [param:SkeletonHelper source], [param:Object options] )</h3>
+		<p></p>
+
+		<h3>[method:this renameBones]( [param:Skeleton skeleton], [param:Array names] )</h3>
+		<p></p>
+
+		<h3>[method:null retarget]( [param:SkeletonHelper target], [param:SkeletonHelper source], [param:Object options] )</h3>
+		<p></p>
+
+		<h3>[method:AnimationClip retargetClip]( [param:SkeletonHelper target], [param:SkeletonHelper source], [param:AnimationClip clip], [param:Object options] )</h3>
+		<p></p>
+
+		<h2>Source</h2>
+
+		[link:https://github.com/mrdoob/three.js/blob/master/examples/js/utils/SkeletonUtils.js examples/js/utils/SkeletonUtils.js]
+	</body>
+</html>

+ 3 - 1
docs/list.js

@@ -363,6 +363,7 @@ var list = {
 
 			"Loaders": {
 				"BabylonLoader": "examples/loaders/BabylonLoader",
+				"DRACOLoader": "examples/loaders/DRACOLoader",
 				"GLTFLoader": "examples/loaders/GLTFLoader",
 				"MMDLoader": "examples/loaders/MMDLoader",
 				"MTLLoader": "examples/loaders/MTLLoader",
@@ -407,7 +408,8 @@ var list = {
 
 			"Utils": {
 				"BufferGeometryUtils": "examples/utils/BufferGeometryUtils",
-				"SceneUtils": "examples/utils/SceneUtils"
+				"SceneUtils": "examples/utils/SceneUtils",
+				"SkeletonUtils": "examples/utils/SkeletonUtils"
 			}
 
 		},

+ 1 - 1
editor/css/dark.css

@@ -294,7 +294,7 @@ select {
 	#toolbar {
 		left: calc(50% - 140px);
 		width: 280px;
-		top: 52px;
+		top: 68px;
 	}
 
 }

+ 1 - 1
editor/css/light.css

@@ -287,7 +287,7 @@ select {
 	#toolbar {
 		left: calc(50% - 140px);
 		width: 280px;
-		top: 52px;
+		top: 68px;
 	}
 
 }

+ 3 - 11
editor/index.html

@@ -157,6 +157,7 @@
 		<script src="js/Strings.js"></script>
 		<script src="js/Toolbar.js"></script>
 		<script src="js/Viewport.js"></script>
+		<script src="js/Viewport.Camera.js"></script>
 		<script src="js/Viewport.Info.js"></script>
 
 		<script src="js/Command.js"></script>
@@ -206,14 +207,11 @@
 			var player = new Player( editor );
 			document.body.appendChild( player.dom );
 
-			var menubar = new Menubar( editor );
-			document.body.appendChild( menubar.dom );
-
 			var sidebar = new Sidebar( editor );
 			document.body.appendChild( sidebar.dom );
 
-			var modal = new UI.Modal();
-			document.body.appendChild( modal.dom );
+			var menubar = new Menubar( editor );
+			document.body.appendChild( menubar.dom );
 
 			//
 
@@ -284,12 +282,6 @@
 				signals.scriptChanged.add( saveState );
 				signals.historyChanged.add( saveState );
 
-				signals.showModal.add( function ( content ) {
-
-					modal.show( content );
-
-				} );
-
 			} );
 
 			//

+ 47 - 6
editor/js/Editor.js

@@ -22,10 +22,6 @@ var Editor = function () {
 		startPlayer: new Signal(),
 		stopPlayer: new Signal(),
 
-		// actions
-
-		showModal: new Signal(),
-
 		// notifications
 
 		editorCleared: new Signal(),
@@ -55,6 +51,9 @@ var Editor = function () {
 		objectChanged: new Signal(),
 		objectRemoved: new Signal(),
 
+		cameraAdded: new Signal(),
+		cameraRemoved: new Signal(),
+
 		helperAdded: new Signal(),
 		helperRemoved: new Signal(),
 
@@ -68,7 +67,9 @@ var Editor = function () {
 
 		showGridChanged: new Signal(),
 		refreshSidebarObject3D: new Signal(),
-		historyChanged: new Signal()
+		historyChanged: new Signal(),
+
+		viewportCameraChanged: new Signal()
 
 	};
 
@@ -99,6 +100,11 @@ var Editor = function () {
 	this.selected = null;
 	this.helpers = {};
 
+	this.cameras = {};
+	this.viewportCamera = this.camera;
+
+	this.addCamera( this.camera );
+
 };
 
 Editor.prototype = {
@@ -149,6 +155,7 @@ Editor.prototype = {
 			if ( child.geometry !== undefined ) scope.addGeometry( child.geometry );
 			if ( child.material !== undefined ) scope.addMaterial( child.material );
 
+			scope.addCamera( child );
 			scope.addHelper( child );
 
 		} );
@@ -199,6 +206,7 @@ Editor.prototype = {
 
 		object.traverse( function ( child ) {
 
+			scope.removeCamera( child );
 			scope.removeHelper( child );
 
 		} );
@@ -254,6 +262,32 @@ Editor.prototype = {
 
 	//
 
+	addCamera: function ( camera ) {
+
+		if ( camera.isCamera ) {
+
+			this.cameras[ camera.uuid ] = camera;
+
+			this.signals.cameraAdded.dispatch( camera );
+
+		}
+
+	},
+
+	removeCamera: function ( camera ) {
+
+		if ( this.cameras[ camera.uuid ] !== undefined ) {
+
+			delete this.cameras[ camera.uuid ];
+
+			this.signals.cameraRemoved.dispatch( camera );
+
+		}
+
+	},
+
+	//
+
 	addHelper: function () {
 
 		var geometry = new THREE.SphereBufferGeometry( 2, 4, 2 );
@@ -285,7 +319,7 @@ Editor.prototype = {
 
 			} else if ( object.isSkinnedMesh ) {
 
-				helper = new THREE.SkeletonHelper( object );
+				helper = new THREE.SkeletonHelper( object.skeleton.bones[ 0 ] );
 
 			} else {
 
@@ -383,6 +417,13 @@ Editor.prototype = {
 
 	},
 
+	setViewportCamera: function ( uuid ) {
+
+		this.viewportCamera = this.cameras[ uuid ];
+		this.signals.viewportCameraChanged.dispatch( this.viewportCamera );
+
+	},
+
 	//
 
 	select: function ( object ) {

+ 1 - 1
editor/js/Loader.js

@@ -453,7 +453,7 @@ var Loader = function ( editor ) {
 					var contents = event.target.result;
 
 					var loader = new THREE.SVGLoader();
-					var paths = loader.parse( contents );
+					var paths = loader.parse( contents ).paths;
 
 					//
 

+ 1 - 1
editor/js/Menubar.Add.js

@@ -380,7 +380,7 @@ Menubar.Add = function ( editor ) {
 	option.setTextContent( strings.getKey( 'menubar/add/perspectivecamera' ) );
 	option.onClick( function () {
 
-		var camera = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
+		var camera = new THREE.PerspectiveCamera();
 		camera.name = 'PerspectiveCamera';
 
 		editor.execute( new AddObjectCommand( camera ) );

+ 1 - 1
editor/js/Sidebar.Settings.js

@@ -72,7 +72,7 @@ Sidebar.Settings = function ( editor ) {
 	themeRow.add( new UI.Text( strings.getKey( 'sidebar/settings/theme' ) ).setWidth( '90px' ) );
 	themeRow.add( theme );
 
-	container.add( themeRow );
+	container.add( themeRow );	
 
 	container.add( new Sidebar.Settings.Shortcuts( editor ) );
 	container.add( new Sidebar.Settings.Viewport( editor ) );

+ 48 - 0
editor/js/Viewport.Camera.js

@@ -0,0 +1,48 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+Viewport.Camera = function ( editor ) {
+
+	var signals = editor.signals;
+
+	//
+
+	var cameraSelect = new UI.Select();
+	cameraSelect.setPosition( 'absolute' );
+	cameraSelect.setRight( '10px' );
+	cameraSelect.setTop( '10px' );
+	cameraSelect.onChange( function () {
+
+		editor.setViewportCamera( this.getValue() );
+
+	} );
+
+	signals.cameraAdded.add( update );
+	signals.cameraRemoved.add( update );
+
+	update();
+
+	//
+
+	function update() {
+
+		var options = {};
+
+		var cameras = editor.cameras;
+
+		for ( var key in cameras ) {
+
+			var camera = cameras[ key ];
+			options[ camera.uuid ] = camera.name;
+
+		}
+
+		cameraSelect.setOptions( options );
+		cameraSelect.setValue( editor.viewportCamera.uuid );
+
+	}
+
+	return cameraSelect;
+
+};

+ 30 - 11
editor/js/Viewport.js

@@ -10,6 +10,7 @@ var Viewport = function ( editor ) {
 	container.setId( 'viewport' );
 	container.setPosition( 'absolute' );
 
+	container.add( new Viewport.Camera( editor ) );
 	container.add( new Viewport.Info( editor ) );
 
 	//
@@ -198,7 +199,7 @@ var Viewport = function ( editor ) {
 
 	function onMouseDown( event ) {
 
-		event.preventDefault();
+		// event.preventDefault();
 
 		var array = getMousePosition( container.dom, event.clientX, event.clientY );
 		onDownPosition.fromArray( array );
@@ -478,16 +479,20 @@ var Viewport = function ( editor ) {
 
 		}
 
-		if ( scene.fog.isFog ) {
+		if ( scene.fog ) {
 
-			scene.fog.color.setHex( fogColor );
-			scene.fog.near = fogNear;
-			scene.fog.far = fogFar;
+			if ( scene.fog.isFog ) {
 
-		} else if ( scene.fog.isFogExp2 ) {
+				scene.fog.color.setHex( fogColor );
+				scene.fog.near = fogNear;
+				scene.fog.far = fogFar;
 
-			scene.fog.color.setHex( fogColor );
-			scene.fog.density = fogDensity;
+			} else if ( scene.fog.isFogExp2 ) {
+
+				scene.fog.color.setHex( fogColor );
+				scene.fog.density = fogDensity;
+
+			}
 
 		}
 
@@ -495,6 +500,17 @@ var Viewport = function ( editor ) {
 
 	} );
 
+	signals.viewportCameraChanged.add( function ( viewportCamera ) {
+
+		camera = viewportCamera;
+
+		camera.aspect = editor.camera.aspect;
+		camera.projectionMatrix.copy( editor.camera.projectionMatrix );
+
+		render();
+
+	} );
+
 	//
 
 	signals.windowResize.add( function () {
@@ -547,14 +563,17 @@ var Viewport = function ( editor ) {
 
 	function render() {
 
-		sceneHelpers.updateMatrixWorld();
 		scene.updateMatrixWorld();
-
 		renderer.render( scene, camera );
 
 		if ( renderer instanceof THREE.RaytracingRenderer === false ) {
 
-			renderer.render( sceneHelpers, camera );
+			if ( camera === editor.camera ) {
+
+				sceneHelpers.updateMatrixWorld();
+				renderer.render( sceneHelpers, camera );
+
+			}
 
 		}
 

+ 51 - 58
editor/js/libs/ui.js

@@ -625,6 +625,56 @@ UI.Number = function ( number ) {
 
 	}
 
+	function onTouchStart( event ) {
+
+		if ( event.touches.length === 1 ) {
+
+			distance = 0;
+
+			onMouseDownValue = scope.value;
+
+			prevPointer = [ event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ];
+
+			document.addEventListener( 'touchmove', onTouchMove, false );
+			document.addEventListener( 'touchend', onTouchEnd, false );
+
+		}
+
+	}
+
+	function onTouchMove( event ) {
+
+		var currentValue = scope.value;
+
+		pointer = [ event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ];
+
+		distance += ( pointer[ 0 ] - prevPointer[ 0 ] ) - ( pointer[ 1 ] - prevPointer[ 1 ] );
+
+		var value = onMouseDownValue + ( distance / ( event.shiftKey ? 5 : 50 ) ) * scope.step;
+		value = Math.min( scope.max, Math.max( scope.min, value ) );
+
+		if ( currentValue !== value ) {
+
+			scope.setValue( value );
+			dom.dispatchEvent( changeEvent );
+
+		}
+
+		prevPointer = [ event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ];
+
+	}
+
+	function onTouchEnd( event ) {
+
+		if ( event.touches.length === 0 ) {
+
+			document.removeEventListener( 'touchmove', onTouchMove, false );
+			document.removeEventListener( 'touchend', onTouchEnd, false );
+
+		}
+
+	}
+
 	function onChange( event ) {
 
 		scope.setValue( dom.value );
@@ -648,6 +698,7 @@ UI.Number = function ( number ) {
 	onBlur();
 
 	dom.addEventListener( 'mousedown', onMouseDown, false );
+	dom.addEventListener( 'touchstart', onTouchStart, false );
 	dom.addEventListener( 'change', onChange, false );
 	dom.addEventListener( 'focus', onFocus, false );
 	dom.addEventListener( 'blur', onBlur, false );
@@ -944,61 +995,3 @@ UI.Button.prototype.setLabel = function ( value ) {
 	return this;
 
 };
-
-
-// Modal
-
-UI.Modal = function ( value ) {
-
-	var scope = this;
-
-	var dom = document.createElement( 'div' );
-
-	dom.style.position = 'absolute';
-	dom.style.width = '100%';
-	dom.style.height = '100%';
-	dom.style.backgroundColor = 'rgba(0,0,0,0.5)';
-	dom.style.display = 'none';
-	dom.style.alignItems = 'center';
-	dom.style.justifyContent = 'center';
-	dom.addEventListener( 'click', function ( event ) {
-
-		scope.hide();
-
-	} );
-
-	this.dom = dom;
-
-	this.container = new UI.Panel();
-	this.container.dom.style.width = '200px';
-	this.container.dom.style.padding = '20px';
-	this.container.dom.style.backgroundColor = '#ffffff';
-	this.container.dom.style.boxShadow = '0px 5px 10px rgba(0,0,0,0.5)';
-
-	this.add( this.container );
-
-	return this;
-
-};
-
-UI.Modal.prototype = Object.create( UI.Element.prototype );
-UI.Modal.prototype.constructor = UI.Modal;
-
-UI.Modal.prototype.show = function ( content ) {
-
-	this.container.clear();
-	this.container.add( content );
-
-	this.dom.style.display = 'flex';
-
-	return this;
-
-};
-
-UI.Modal.prototype.hide = function () {
-
-	this.dom.style.display = 'none';
-
-	return this;
-
-};

+ 1 - 1
editor/sw.js

@@ -1,4 +1,4 @@
-// r102.1
+// r103
 
 const staticAssets = [
 	'./',

+ 50 - 39
examples/files.js

@@ -4,6 +4,7 @@ var files = {
 		"webgl_animation_keyframes",
 		"webgl_animation_skinning_blending",
 		"webgl_animation_skinning_morph",
+		"webgl_animation_multiple",
 		"webgl_camera",
 		"webgl_camera_array",
 		"webgl_camera_cinematic",
@@ -19,7 +20,6 @@ var files = {
 		"webgl_effects_peppersghost",
 		"webgl_effects_stereo",
 		"webgl_framebuffer_texture",
-		"webgl_furnace_test",
 		"webgl_geometries",
 		"webgl_geometries_parametric",
 		"webgl_geometry_colors",
@@ -44,6 +44,7 @@ var files = {
 		"webgl_geometry_terrain_raycast",
 		"webgl_geometry_text",
 		"webgl_geometry_text_shapes",
+		"webgl_geometry_text_stroke",
 		"webgl_hdr",
 		"webgl_helpers",
 		"webgl_interactive_buffergeometry",
@@ -67,6 +68,7 @@ var files = {
 		"webgl_lights_spotlight",
 		"webgl_lights_spotlights",
 		"webgl_lights_rectarealight",
+		"webgl_lightshafts",
 		"webgl_lines_colors",
 		"webgl_lines_dashed",
 		"webgl_lines_fat",
@@ -95,6 +97,7 @@ var files = {
 		"webgl_loader_json_claraio",
 		"webgl_loader_kmz",
 		"webgl_loader_ldraw",
+		"webgl_loader_lwo",
 		"webgl_loader_md2",
 		"webgl_loader_md2_control",
 		"webgl_loader_mmd",
@@ -155,6 +158,7 @@ var files = {
 		"webgl_materials_envmaps",
 		"webgl_materials_envmaps_exr",
 		"webgl_materials_envmaps_hdr",
+		"webgl_materials_envmaps_parallax",
 		"webgl_materials_grass",
 		"webgl_materials_lightmap",
 		"webgl_materials_matcap",
@@ -208,40 +212,10 @@ var files = {
 		"webgl_performance_doublesided",
 		"webgl_performance_nodes",
 		"webgl_performance_static",
-		"webgl_physics_cloth",
-		"webgl_physics_convex_break",
-		"webgl_physics_rope",
-		"webgl_physics_terrain",
-		"webgl_physics_volume",
 		"webgl_points_billboards",
 		"webgl_points_dynamic",
 		"webgl_points_sprites",
 		"webgl_points_waves",
-		"webgl_postprocessing",
-		"webgl_postprocessing_advanced",
-		"webgl_postprocessing_afterimage",
-		"webgl_postprocessing_backgrounds",
-		"webgl_postprocessing_crossfade",
-		"webgl_postprocessing_dof",
-		"webgl_postprocessing_dof2",
-		"webgl_postprocessing_fxaa",
-		"webgl_postprocessing_glitch",
-		"webgl_postprocessing_godrays",
-		"webgl_postprocessing_rgb_halftone",
-		"webgl_postprocessing_masking",
-		"webgl_postprocessing_ssaa",
-		"webgl_postprocessing_ssaa_unbiased",
-		"webgl_postprocessing_nodes",
-		"webgl_postprocessing_nodes_pass",
-		"webgl_postprocessing_outline",
-		"webgl_postprocessing_pixel",
-		"webgl_postprocessing_procedural",
-		"webgl_postprocessing_sao",
-		"webgl_postprocessing_smaa",
-		"webgl_postprocessing_sobel",
-		"webgl_postprocessing_ssao",
-		"webgl_postprocessing_taa",
-		"webgl_postprocessing_unreal_bloom",
 		"webgl_raycast_sprite",
 		"webgl_raycast_texture",
 		"webgl_read_float_buffer",
@@ -274,6 +248,33 @@ var files = {
 		"webgl_water",
 		"webgl_water_flowmap"
 	],
+	"webgl / postprocessing": [
+		"webgl_postprocessing",
+		"webgl_postprocessing_advanced",
+		"webgl_postprocessing_afterimage",
+		"webgl_postprocessing_backgrounds",
+		"webgl_postprocessing_crossfade",
+		"webgl_postprocessing_dof",
+		"webgl_postprocessing_dof2",
+		"webgl_postprocessing_fxaa",
+		"webgl_postprocessing_glitch",
+		"webgl_postprocessing_godrays",
+		"webgl_postprocessing_rgb_halftone",
+		"webgl_postprocessing_masking",
+		"webgl_postprocessing_ssaa",
+		"webgl_postprocessing_ssaa_unbiased",
+		"webgl_postprocessing_nodes",
+		"webgl_postprocessing_nodes_pass",
+		"webgl_postprocessing_outline",
+		"webgl_postprocessing_pixel",
+		"webgl_postprocessing_procedural",
+		"webgl_postprocessing_sao",
+		"webgl_postprocessing_smaa",
+		"webgl_postprocessing_sobel",
+		"webgl_postprocessing_ssao",
+		"webgl_postprocessing_taa",
+		"webgl_postprocessing_unreal_bloom"
+	],
 	"webgl / advanced": [
 		"webgl_buffergeometry",
 		"webgl_buffergeometry_constructed_from_geometry",
@@ -309,14 +310,13 @@ var files = {
 		"webgl_shadowmap_pcss",
 		"webgl_simple_gi",
 		"webgl_tiled_forward",
-		"webgl_worker_offscreencanvas"
-	],
-	"webgl deferred": [
+		"webgl_worker_offscreencanvas",
 		"webgldeferred_animation"
 	],
 	"webgl2": [
+		"webgl2_loader_gltf",
+		"webgl2_materials_texture2darray",
 		"webgl2_materials_texture3d",
-		"webgl2_materials_texture3d_volume",
 		"webgl2_multisampled_renderbuffers",
 		"webgl2_sandbox"
 	],
@@ -340,6 +340,13 @@ var files = {
 		"webvr_vive_paint",
 		"webvr_vive_sculpt"
 	],
+	"physics": [
+		"webgl_physics_cloth",
+		"webgl_physics_convex_break",
+		"webgl_physics_rope",
+		"webgl_physics_terrain",
+		"webgl_physics_volume"
+	],
 	"misc": [
 		"misc_animation_authoring",
 		"misc_animation_groups",
@@ -353,12 +360,15 @@ var files = {
 		"misc_controls_trackball",
 		"misc_controls_transform",
 		"misc_exporter_collada",
+		"misc_exporter_draco",
 		"misc_exporter_gltf",
 		"misc_exporter_obj",
 		"misc_exporter_stl",
 		"misc_fps",
 		"misc_lookat",
-		"misc_uv_tests"
+	],
+	"css2d": [
+		"css2d_label"
 	],
 	"css3d": [
 		"css3d_molecules",
@@ -370,9 +380,6 @@ var files = {
 		"css3d_sprites",
 		"css3d_youtube"
 	],
-	"css2d": [
-		"css2d_label"
-	],
 	"raytracing": [
 		"raytracing_sandbox"
 	],
@@ -384,5 +391,9 @@ var files = {
 	"svg": [
 		"svg_lines",
 		"svg_sandbox"
+	],
+	"tests": [
+		"webgl_furnace_test",
+		"misc_uv_tests"
 	]
 };

+ 4 - 2
examples/js/MD2Character.js

@@ -44,8 +44,10 @@ THREE.MD2Character = function () {
 
 		loader.load( config.baseUrl + config.body, function ( geo ) {
 
-			geo.computeBoundingBox();
-			scope.root.position.y = - scope.scale * geo.boundingBox.min.y;
+			var boundingBox = new THREE.Box3();
+			boundingBox.setFromBufferAttribute( geo.attributes.position );
+			
+			scope.root.position.y = - scope.scale * boundingBox.min.y;
 
 			var mesh = createPart( geo, scope.skinsBody[ 0 ] );
 			mesh.scale.set( scope.scale, scope.scale, scope.scale );

+ 4 - 2
examples/js/MD2CharacterComplex.js

@@ -156,8 +156,10 @@ THREE.MD2CharacterComplex = function () {
 
 		loader.load( config.baseUrl + config.body, function( geo ) {
 
-			geo.computeBoundingBox();
-			scope.root.position.y = - scope.scale * geo.boundingBox.min.y;
+			var boundingBox = new THREE.Box3();
+			boundingBox.setFromBufferAttribute( geo.attributes.position );
+
+			scope.root.position.y = - scope.scale * boundingBox.min.y;
 
 			var mesh = createPart( geo, scope.skinsBody[ 0 ] );
 			mesh.scale.set( scope.scale, scope.scale, scope.scale );

+ 73 - 0
examples/js/MarchingCubes.js

@@ -772,6 +772,79 @@ THREE.MarchingCubes = function ( resolution, material, enableUvs, enableColors )
 	// Updates
 	/////////////////////////////////////
 
+	this.setCell = function ( x, y, z, value ) {
+
+		var index = this.size2 * z + this.size * y + x;
+		this.field[ index ] = value;
+
+	};
+
+	this.getCell = function ( x, y, z ) {
+
+		var index = this.size2 * z + this.size * y + x;
+		return this.field[ index ];
+
+	};
+
+	this.blur = function ( intensity ) {
+
+		if ( intensity === undefined ) {
+
+			intensity = 1;
+
+		}
+
+		var field = this.field;
+		var fieldCopy = field.slice();
+		var size = this.size;
+		var size2 = this.size2;
+		for ( var x = 0; x < size; x ++ ) {
+
+			for ( var y = 0; y < size; y ++ ) {
+
+				for ( var z = 0; z < size; z ++ ) {
+
+					var index = size2 * z + size * y + x;
+					var val = fieldCopy[ index ];
+					var count = 1;
+
+					for ( var x2 = - 1; x2 <= 1; x2 += 2 ) {
+
+						var x3 = x2 + x;
+						if ( x3 < 0 || x3 >= size ) continue;
+
+						for ( var y2 = - 1; y2 <= 1; y2 += 2 ) {
+
+							var y3 = y2 + y;
+							if ( y3 < 0 || y3 >= size ) continue;
+
+							for ( var z2 = - 1; z2 <= 1; z2 += 2 ) {
+
+								var z3 = z2 + z;
+								if ( z3 < 0 || z3 >= size ) continue;
+
+								var index2 = size2 * z3 + size * y3 + x3;
+								var val2 = fieldCopy[ index2 ];
+
+								count ++;
+								val += intensity * ( val2 - val ) / count;
+
+							}
+
+						}
+
+					}
+
+					field[ index ] = val;
+
+				}
+
+			}
+
+		}
+
+	};
+
 	this.reset = function () {
 
 		var i;

+ 14 - 14
examples/js/controls/EditorControls.js

@@ -13,7 +13,7 @@ THREE.EditorControls = function ( object, domElement ) {
 
 	this.enabled = true;
 	this.center = new THREE.Vector3();
-	this.panSpeed = 0.001;
+	this.panSpeed = 0.002;
 	this.zoomSpeed = 0.1;
 	this.rotationSpeed = 0.005;
 
@@ -104,8 +104,8 @@ THREE.EditorControls = function ( object, domElement ) {
 
 		spherical.setFromVector3( vector );
 
-		spherical.theta += delta.x;
-		spherical.phi += delta.y;
+		spherical.theta += delta.x * scope.rotationSpeed;
+		spherical.phi += delta.y * scope.rotationSpeed;
 
 		spherical.makeSafe();
 
@@ -159,7 +159,7 @@ THREE.EditorControls = function ( object, domElement ) {
 
 		if ( state === STATE.ROTATE ) {
 
-			scope.rotate( delta.set( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );
+			scope.rotate( delta.set( - movementX, - movementY, 0 ) );
 
 		} else if ( state === STATE.ZOOM ) {
 
@@ -235,13 +235,13 @@ THREE.EditorControls = function ( object, domElement ) {
 		switch ( event.touches.length ) {
 
 			case 1:
-				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
-				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
+				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
+				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
 				break;
 
 			case 2:
-				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
-				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
+				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
+				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
 				prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
 				break;
 
@@ -277,14 +277,14 @@ THREE.EditorControls = function ( object, domElement ) {
 		switch ( event.touches.length ) {
 
 			case 1:
-				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
-				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
-				scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
+				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
+				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
+				scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - 1 ) );
 				break;
 
 			case 2:
-				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
-				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
+				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
+				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 ).divideScalar( window.devicePixelRatio );
 				var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
 				scope.zoom( delta.set( 0, 0, prevDistance - distance ) );
 				prevDistance = distance;
@@ -295,7 +295,7 @@ THREE.EditorControls = function ( object, domElement ) {
 				offset0.x = - offset0.x;
 				offset1.x = - offset1.x;
 
-				scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );
+				scope.pan( offset0.add( offset1 ) );
 
 				break;
 

+ 13 - 7
examples/js/controls/TransformControls.js

@@ -396,27 +396,33 @@ THREE.TransformControls = function ( camera, domElement ) {
 
 				if ( pointEnd.dot( pointStart ) < 0 ) d *= -1;
 
-				_tempVector.set( d, d, d );
+				_tempVector2.set( d, d, d );
 
 			} else {
 
-				_tempVector.copy( pointEnd ).divide( pointStart );
+				_tempVector.copy(pointStart);
+				_tempVector2.copy(pointEnd);
+
+				_tempVector.applyQuaternion( worldQuaternionInv );
+				_tempVector2.applyQuaternion( worldQuaternionInv );
+
+				_tempVector2.divide( _tempVector );
 
 				if ( axis.search( 'X' ) === -1 ) {
-					_tempVector.x = 1;
+					_tempVector2.x = 1;
 				}
 				if ( axis.search( 'Y' ) === -1 ) {
-					_tempVector.y = 1;
+					_tempVector2.y = 1;
 				}
 				if ( axis.search( 'Z' ) === -1 ) {
-					_tempVector.z = 1;
+					_tempVector2.z = 1;
 				}
 
 			}
 
 			// Apply scale
 
-			object.scale.copy( scaleStart ).multiply( _tempVector );
+			object.scale.copy( scaleStart ).multiply( _tempVector2 );
 
 		} else if ( mode === 'rotate' ) {
 
@@ -1183,7 +1189,7 @@ THREE.TransformControlsGizmo = function () {
 
 				var AXIS_HIDE_TRESHOLD = 0.99;
 				var PLANE_HIDE_TRESHOLD = 0.2;
-				var AXIS_FLIP_TRESHOLD = -0.4;
+				var AXIS_FLIP_TRESHOLD = 0.0;
 
 
 				if ( handle.name === 'X' || handle.name === 'XYZX' ) {

+ 206 - 0
examples/js/exporters/DracoExporter.js

@@ -0,0 +1,206 @@
+'use strict';
+
+/**
+ * Export draco compressed files from threejs geometry objects.
+ *
+ * Draco files are compressed and usually are smaller than conventional 3D file formats.
+ *
+ * The exporter receives a options object containing
+ *  - decodeSpeed, indicates how to tune the encoder regarding decode speed (0 gives better speed but worst quality)
+ *  - encodeSpeed, indicates how to tune the encoder parameters (0 gives better speed but worst quality)
+ *  - encoderMethod
+ *  - quantization, indicates the presision of each type of data stored in the draco file in the order (POSITION, NORMAL, COLOR, TEX_COORD, GENERIC)
+ *  - exportUvs
+ *  - exportNormals
+ *
+ * @class DRACOExporter
+ * @author tentone
+ */
+
+THREE.DRACOExporter = function () {};
+
+THREE.DRACOExporter.prototype = {
+
+	constructor: THREE.DRACOExporter,
+
+	parse: function ( geometry, options ) {
+
+
+		if ( DracoEncoderModule === undefined ) {
+
+			throw new Error( 'THREE.DRACOExporter: required the draco_decoder to work.' );
+
+		}
+
+		if ( options === undefined ) {
+
+			options = {
+
+				decodeSpeed: 5,
+				encodeSpeed: 5,
+				encoderMethod: THREE.DRACOExporter.MESH_EDGEBREAKER_ENCODING,
+				quantization: [ 16, 8, 8, 8, 8 ],
+				exportUvs: true,
+				exportNormals: true,
+				exportColor: false,
+
+			};
+
+		}
+
+		var dracoEncoder = DracoEncoderModule();
+		var encoder = new dracoEncoder.Encoder();
+		var builder = new dracoEncoder.MeshBuilder();
+		var mesh = new dracoEncoder.Mesh();
+
+		if ( geometry.isGeometry === true ) {
+
+			var bufferGeometry = new THREE.BufferGeometry();
+			bufferGeometry.fromGeometry( geometry );
+			geometry = bufferGeometry;
+
+		}
+
+		if ( geometry.isBufferGeometry !== true ) {
+
+			throw new Error( 'THREE.DRACOExporter.parse(geometry, options): geometry is not a THREE.Geometry or THREE.BufferGeometry instance.' );
+
+		}
+
+		var vertices = geometry.getAttribute( 'position' );
+		builder.AddFloatAttributeToMesh( mesh, dracoEncoder.POSITION, vertices.count, vertices.itemSize, vertices.array );
+
+		var faces = geometry.getIndex();
+
+		if ( faces !== null ) {
+
+			builder.AddFacesToMesh( mesh, faces.count, faces.array );
+
+		} else {
+
+			var faces = new ( vertices.count > 65535 ? Uint32Array : Uint16Array ) ( vertices.count );
+
+			for ( var i = 0; i < faces.length; i ++ ) {
+
+				faces[ i ] = i;
+
+			}
+
+			builder.AddFacesToMesh( mesh, vertices.count, faces );
+
+		}
+
+		if ( options.exportNormals === true ) {
+
+			var normals = geometry.getAttribute( 'normal' );
+
+			if ( normals !== undefined ) {
+
+				builder.AddFloatAttributeToMesh( mesh, dracoEncoder.NORMAL, normals.count, normals.itemSize, normals.array );
+
+			}
+
+		}
+
+		if ( options.exportUvs === true ) {
+
+			var uvs = geometry.getAttribute( 'uv' );
+
+			if ( uvs !== undefined ) {
+
+				builder.AddFloatAttributeToMesh( mesh, dracoEncoder.TEX_COORD, uvs.count, uvs.itemSize, uvs.array );
+
+			}
+
+		}
+
+		if ( options.exportColor === true ) {
+
+			var colors = geometry.getAttribute( 'color' );
+
+			if ( colors !== undefined ) {
+
+				builder.AddFloatAttributeToMesh( mesh, dracoEncoder.COLOR, colors.count, colors.itemSize, colors.array );
+
+			}
+
+		}
+
+		//Compress using draco encoder
+
+		var encodedData = new dracoEncoder.DracoInt8Array();
+
+		//Sets the desired encoding and decoding speed for the given options from 0 (slowest speed, but the best compression) to 10 (fastest, but the worst compression).
+
+		encoder.SetSpeedOptions( options.encodeSpeed || 5, options.decodeSpeed || 5 );
+
+		// Sets the desired encoding method for a given geometry.
+
+		if ( options.encoderMethod !== undefined ) {
+
+			encoder.SetEncodingMethod( options.encoderMethod );
+
+		}
+
+		// Sets the quantization (number of bits used to represent) compression options for a named attribute.
+		// The attribute values will be quantized in a box defined by the maximum extent of the attribute values.
+		if ( options.quantization !== undefined ) {
+
+			for ( var i = 0; i < 5; i ++ ) {
+
+				if ( options.quantization[ i ] !== undefined ) {
+
+					encoder.SetAttributeQuantization( i, options.quantization[ i ] );
+
+				}
+
+			}
+
+		}
+
+		var length = encoder.EncodeMeshToDracoBuffer( mesh, encodedData );
+		dracoEncoder.destroy( mesh );
+
+		if ( length === 0 ) {
+
+			throw new Error( 'THREE.DRACOExporter: Draco encoding failed.' );
+
+		}
+
+		//Copy encoded data to buffer.
+		var outputData = new Int8Array( new ArrayBuffer( length ) );
+
+		for ( var i = 0; i < length; i ++ ) {
+
+			outputData[ i ] = encodedData.GetValue( i );
+
+		}
+
+		dracoEncoder.destroy( encodedData );
+		dracoEncoder.destroy( encoder );
+		dracoEncoder.destroy( builder );
+
+		return outputData;
+
+	}
+
+};
+
+// Encoder methods
+
+THREE.DRACOExporter.MESH_EDGEBREAKER_ENCODING = 1;
+THREE.DRACOExporter.MESH_SEQUENTIAL_ENCODING = 0;
+
+// Geometry type
+
+THREE.DRACOExporter.POINT_CLOUD = 0;
+THREE.DRACOExporter.TRIANGULAR_MESH = 1;
+
+// Attribute type
+
+THREE.DRACOExporter.INVALID = - 1;
+THREE.DRACOExporter.POSITION = 0;
+THREE.DRACOExporter.NORMAL = 1;
+THREE.DRACOExporter.COLOR = 2;
+THREE.DRACOExporter.TEX_COORD = 3;
+THREE.DRACOExporter.GENERIC = 4;

+ 62 - 29
examples/js/exporters/GLTFExporter.js

@@ -80,7 +80,8 @@ THREE.GLTFExporter.prototype = {
 			embedImages: true,
 			animations: [],
 			forceIndices: false,
-			forcePowerOfTwoTextures: false
+			forcePowerOfTwoTextures: false,
+			includeCustomExtensions: false
 		};
 
 		options = Object.assign( {}, DEFAULT_OPTIONS, options );
@@ -340,21 +341,50 @@ THREE.GLTFExporter.prototype = {
 		 * Serializes a userData.
 		 *
 		 * @param {THREE.Object3D|THREE.Material} object
-		 * @returns {Object}
+		 * @param {Object} gltfProperty
 		 */
-		function serializeUserData( object ) {
+		function serializeUserData( object, gltfProperty ) {
+
+			if ( Object.keys( object.userData ).length === 0 ) {
+
+				return;
+
+			}
 
 			try {
 
-				return JSON.parse( JSON.stringify( object.userData ) );
+				var json = JSON.parse( JSON.stringify( object.userData ) );
+
+				if ( options.includeCustomExtensions && json.gltfExtensions ) {
+
+					if ( gltfProperty.extensions === undefined ) {
+
+						gltfProperty.extensions = {};
+
+					}
+
+					for ( var extensionName in json.gltfExtensions ) {
+
+						gltfProperty.extensions[ extensionName ] = json.gltfExtensions[ extensionName ];
+						extensionsUsed[ extensionName ] = true;
+
+					}
+
+					delete json.gltfExtensions;
+
+				}
+
+				if ( Object.keys( json ).length > 0 ) {
+
+					gltfProperty.extras = json;
+
+				}
 
 			} catch ( error ) {
 
 				console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
 					'won\'t be serialized because of JSON.stringify error - ' + error.message );
 
-				return {};
-
 			}
 
 		}
@@ -1025,11 +1055,7 @@ THREE.GLTFExporter.prototype = {
 
 			}
 
-			if ( Object.keys( material.userData ).length > 0 ) {
-
-				gltfMaterial.extras = serializeUserData( material );
-
-			}
+			serializeUserData( material, gltfMaterial );
 
 			outputJSON.materials.push( gltfMaterial );
 
@@ -1136,9 +1162,22 @@ THREE.GLTFExporter.prototype = {
 			var modifiedAttribute = null;
 			for ( var attributeName in geometry.attributes ) {
 
+				// Ignore morph target attributes, which are exported later.
+				if ( attributeName.substr( 0, 5 ) === 'morph' ) continue;
+
 				var attribute = geometry.attributes[ attributeName ];
 				attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
 
+				// Prefix all geometry attributes except the ones specifically
+				// listed in the spec; non-spec attributes are considered custom.
+				var validVertexAttributes =
+						/^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;
+				if ( ! validVertexAttributes.test( attributeName ) ) {
+
+					attributeName = '_' + attributeName;
+
+				}
+
 				if ( cachedData.attributes.has( attribute ) ) {
 
 					attributes[ attributeName ] = cachedData.attributes.get( attribute );
@@ -1158,15 +1197,11 @@ THREE.GLTFExporter.prototype = {
 
 				}
 
-				if ( attributeName.substr( 0, 5 ) !== 'MORPH' ) {
+				var accessor = processAccessor( modifiedAttribute || attribute, geometry );
+				if ( accessor !== null ) {
 
-					var accessor = processAccessor( modifiedAttribute || attribute, geometry );
-					if ( accessor !== null ) {
-
-						attributes[ attributeName ] = accessor;
-						cachedData.attributes.set( attribute, accessor );
-
-					}
+					attributes[ attributeName ] = accessor;
+					cachedData.attributes.set( attribute, accessor );
 
 				}
 
@@ -1276,8 +1311,6 @@ THREE.GLTFExporter.prototype = {
 
 			}
 
-			var extras = ( Object.keys( geometry.userData ).length > 0 ) ? serializeUserData( geometry ) : undefined;
-
 			var forceIndices = options.forceIndices;
 			var isMultiMaterial = Array.isArray( mesh.material );
 
@@ -1319,7 +1352,7 @@ THREE.GLTFExporter.prototype = {
 					attributes: attributes,
 				};
 
-				if ( extras ) primitive.extras = extras;
+				serializeUserData( geometry, primitive );
 
 				if ( targets.length > 0 ) primitive.targets = targets;
 
@@ -1336,6 +1369,8 @@ THREE.GLTFExporter.prototype = {
 
 					}
 
+					if ( primitive.indices === null ) delete primitive.indices;
+
 				}
 
 				var material = processMaterial( materials[ groups[ i ].materialIndex ] );
@@ -1699,11 +1734,7 @@ THREE.GLTFExporter.prototype = {
 
 			}
 
-			if ( object.userData && Object.keys( object.userData ).length > 0 ) {
-
-				gltfNode.extras = serializeUserData( object );
-
-			}
+			serializeUserData( object, gltfNode );
 
 			if ( object.isMesh || object.isLine || object.isPoints ) {
 
@@ -1734,7 +1765,7 @@ THREE.GLTFExporter.prototype = {
 
 			} else if ( object.isLight ) {
 
-				console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.' );
+				console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', object );
 				return null;
 
 			}
@@ -1844,6 +1875,8 @@ THREE.GLTFExporter.prototype = {
 
 			}
 
+			serializeUserData( scene, gltfScene );
+
 		}
 
 		/**
@@ -2125,7 +2158,7 @@ THREE.GLTFExporter.Utils = {
 				console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
 
 				sourceTrack = sourceTrack.clone();
-				sourceTrack.setInterpolation( InterpolateLinear );
+				sourceTrack.setInterpolation( THREE.InterpolateLinear );
 
 			}
 

File diff suppressed because it is too large
+ 3 - 0
examples/js/libs/draco/draco_encoder.js


+ 2 - 2
examples/js/loaders/GCodeLoader.js

@@ -146,7 +146,7 @@ THREE.GCodeLoader.prototype.parse = function ( data ) {
 		} else if ( cmd === 'G2' || cmd === 'G3' ) {
 
 			//G2/G3 - Arc Movement ( G2 clock wise and G3 counter clock wise )
-			console.warn( 'THREE.GCodeLoader: Arc command not supported' );
+			//console.warn( 'THREE.GCodeLoader: Arc command not supported' );
 
 		} else if ( cmd === 'G90' ) {
 
@@ -170,7 +170,7 @@ THREE.GCodeLoader.prototype.parse = function ( data ) {
 
 		} else {
 
-			console.warn( 'THREE.GCodeLoader: Command not supported:' + cmd );
+			//console.warn( 'THREE.GCodeLoader: Command not supported:' + cmd );
 
 		}
 

+ 183 - 403
examples/js/loaders/GLTFLoader.js

@@ -218,23 +218,7 @@ THREE.GLTFLoader = ( function () {
 
 			} );
 
-			parser.parse( function ( scene, scenes, cameras, animations, json ) {
-
-				var glTF = {
-					scene: scene,
-					scenes: scenes,
-					cameras: cameras,
-					animations: animations,
-					asset: json.asset,
-					parser: parser,
-					userData: {}
-				};
-
-				addUnknownExtensionsToUserData( extensions, glTF, json );
-
-				onLoad( glTF );
-
-			}, onError );
+			parser.parse( onLoad, onError );
 
 		}
 
@@ -512,7 +496,6 @@ THREE.GLTFLoader = ( function () {
 		this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
 		this.json = json;
 		this.dracoLoader = dracoLoader;
-		THREE.DRACOLoader.getDecoderModule();
 
 	}
 
@@ -528,21 +511,23 @@ THREE.GLTFLoader = ( function () {
 
 		for ( var attributeName in gltfAttributeMap ) {
 
-			if ( ! ( attributeName in ATTRIBUTES ) ) continue;
+			var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();
 
-			threeAttributeMap[ ATTRIBUTES[ attributeName ] ] = gltfAttributeMap[ attributeName ];
+			threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ];
 
 		}
 
 		for ( attributeName in primitive.attributes ) {
 
-			if ( ATTRIBUTES[ attributeName ] !== undefined && gltfAttributeMap[ attributeName ] !== undefined ) {
+			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[ ATTRIBUTES[ attributeName ] ] = componentType;
-				attributeNormalizedMap[ ATTRIBUTES[ attributeName ] ] = accessorDef.normalized === true;
+				attributeTypeMap[ threeAttributeName ] = componentType;
+				attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true;
 
 			}
 
@@ -1218,10 +1203,8 @@ THREE.GLTFLoader = ( function () {
 	};
 
 	var INTERPOLATION = {
-		CUBICSPLINE: THREE.InterpolateSmooth, // We use custom interpolation GLTFCubicSplineInterpolation for CUBICSPLINE.
-		                                      // KeyframeTrack.optimize() can't handle glTF Cubic Spline output values layout,
-		                                      // using THREE.InterpolateSmooth for KeyframeTrack instantiation to prevent optimization.
-		                                      // See KeyframeTrack.optimize() for the detail.
+		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: THREE.InterpolateLinear,
 		STEP: THREE.InterpolateDiscrete
 	};
@@ -1267,12 +1250,14 @@ THREE.GLTFLoader = ( function () {
 
 	}
 
+	var defaultMaterial;
+
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
 	 */
 	function createDefaultMaterial() {
 
-		return new THREE.MeshStandardMaterial( {
+		defaultMaterial = defaultMaterial || new THREE.MeshStandardMaterial( {
 			color: 0xFFFFFF,
 			emissive: 0x000000,
 			metalness: 1,
@@ -1282,6 +1267,8 @@ THREE.GLTFLoader = ( function () {
 			side: THREE.FrontSide
 		} );
 
+		return defaultMaterial;
+
 	}
 
 	function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) {
@@ -1358,34 +1345,21 @@ THREE.GLTFLoader = ( function () {
 
 			if ( hasMorphPosition ) {
 
-				// TODO: Error-prone use of a callback inside a loop.
-				var accessor = target.POSITION !== undefined
+				var pendingAccessor = target.POSITION !== undefined
 					? parser.getDependency( 'accessor', target.POSITION )
-						.then( function ( accessor ) {
-
-							// Cloning not to pollute original accessor below
-							return cloneBufferAttribute( accessor );
-
-						} )
 					: geometry.attributes.position;
 
-				pendingPositionAccessors.push( accessor );
+				pendingPositionAccessors.push( pendingAccessor );
 
 			}
 
 			if ( hasMorphNormal ) {
 
-				// TODO: Error-prone use of a callback inside a loop.
-				var accessor = target.NORMAL !== undefined
+				var pendingAccessor = target.NORMAL !== undefined
 					? parser.getDependency( 'accessor', target.NORMAL )
-						.then( function ( accessor ) {
-
-							return cloneBufferAttribute( accessor );
-
-						} )
 					: geometry.attributes.normal;
 
-				pendingNormalAccessors.push( accessor );
+				pendingNormalAccessors.push( pendingAccessor );
 
 			}
 
@@ -1399,6 +1373,24 @@ THREE.GLTFLoader = ( function () {
 			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 ];
@@ -1570,34 +1562,6 @@ THREE.GLTFLoader = ( function () {
 
 	}
 
-	function createArrayKeyBufferGeometry( a ) {
-
-		var arrayKey = '';
-
-		for ( var i = 0, il = a.length; i < il; i ++ ) {
-
-			arrayKey += ':' + a[ i ].uuid;
-
-		}
-
-		return arrayKey;
-
-	}
-
-	function createMultiPassGeometryKey( geometry, primitives ) {
-
-		var key = geometry.uuid;
-
-		for ( var i = 0, il = primitives.length; i < il; i ++ ) {
-
-			key += i + createPrimitiveKey( primitives[ i ] );
-
-		}
-
-		return key;
-
-	}
-
 	function cloneBufferAttribute( attribute ) {
 
 		if ( attribute.isInterleavedBufferAttribute ) {
@@ -1623,48 +1587,6 @@ THREE.GLTFLoader = ( function () {
 
 	}
 
-	/**
-	 * Checks if we can build a single Mesh with MultiMaterial from multiple primitives.
-	 * Returns true if all primitives use the same attributes/morphAttributes/mode
-	 * and also have index. Otherwise returns false.
-	 *
-	 * @param {Array<GLTF.Primitive>} primitives
-	 * @return {Boolean}
-	 */
-	function isMultiPassGeometry( primitives ) {
-
-		if ( primitives.length < 2 ) return false;
-
-		var primitive0 = primitives[ 0 ];
-		var targets0 = primitive0.targets || [];
-
-		if ( primitive0.indices === undefined ) return false;
-
-		for ( var i = 1, il = primitives.length; i < il; i ++ ) {
-
-			var primitive = primitives[ i ];
-
-			if ( primitive0.mode !== primitive.mode ) return false;
-			if ( primitive.indices === undefined ) return false;
-			if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) return false;
-			if ( ! isObjectEqual( primitive0.attributes, primitive.attributes ) ) return false;
-
-			var targets = primitive.targets || [];
-
-			if ( targets0.length !== targets.length ) return false;
-
-			for ( var j = 0, jl = targets0.length; j < jl; j ++ ) {
-
-				if ( ! isObjectEqual( targets0[ j ], targets[ j ] ) ) return false;
-
-			}
-
-		}
-
-		return true;
-
-	}
-
 	/* GLTF PARSER */
 
 	function GLTFParser( json, extensions, options ) {
@@ -1678,8 +1600,6 @@ THREE.GLTFLoader = ( function () {
 
 		// BufferGeometry caching
 		this.primitiveCache = {};
-		this.multiplePrimitivesCache = {};
-		this.multiPassGeometryCache = {};
 
 		this.textureLoader = new THREE.TextureLoader( this.options.manager );
 		this.textureLoader.setCrossOrigin( this.options.crossOrigin );
@@ -1691,7 +1611,9 @@ THREE.GLTFLoader = ( function () {
 
 	GLTFParser.prototype.parse = function ( onLoad, onError ) {
 
+		var parser = this;
 		var json = this.json;
+		var extensions = this.extensions;
 
 		// Clear the loader cache
 		this.cache.removeAll();
@@ -1699,21 +1621,27 @@ THREE.GLTFLoader = ( function () {
 		// Mark the special nodes/meshes in json for efficient parse
 		this.markDefs();
 
-		// Fire the callback on complete
-		this.getMultiDependencies( [
+		Promise.all( [
 
-			'scene',
-			'animation',
-			'camera'
+			this.getDependencies( 'scene' ),
+			this.getDependencies( 'animation' ),
+			this.getDependencies( 'camera' ),
 
 		] ).then( function ( dependencies ) {
 
-			var scenes = dependencies.scenes || [];
-			var scene = scenes[ json.scene || 0 ];
-			var animations = dependencies.animations || [];
-			var cameras = dependencies.cameras || [];
+			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 );
 
-			onLoad( scene, scenes, cameras, animations, json );
+			onLoad( result );
 
 		} ).catch( onError );
 
@@ -1886,40 +1814,6 @@ THREE.GLTFLoader = ( function () {
 
 	};
 
-	/**
-	 * Requests all multiple dependencies of the specified types asynchronously, with caching.
-	 * @param {Array<string>} types
-	 * @return {Promise<Object<Array<Object>>>}
-	 */
-	GLTFParser.prototype.getMultiDependencies = function ( types ) {
-
-		var results = {};
-		var pending = [];
-
-		for ( var i = 0, il = types.length; i < il; i ++ ) {
-
-			var type = types[ i ];
-			var value = this.getDependencies( type );
-
-			// TODO: Error-prone use of a callback inside a loop.
-			value = value.then( function ( key, value ) {
-
-				results[ key ] = value;
-
-			}.bind( this, type + ( type === 'mesh' ? 'es' : 's' ) ) );
-
-			pending.push( value );
-
-		}
-
-		return Promise.all( pending ).then( function () {
-
-			return results;
-
-		} );
-
-	};
-
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
 	 * @param {number} bufferIndex
@@ -2252,6 +2146,124 @@ THREE.GLTFLoader = ( function () {
 
 	};
 
+	/**
+	 * 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  {THREE.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 THREE.PointsMaterial();
+				THREE.Material.prototype.copy.call( pointsMaterial, material );
+				pointsMaterial.color.copy( material.color );
+				pointsMaterial.map = material.map;
+				pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
+
+				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 THREE.LineBasicMaterial();
+				THREE.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 = THREE.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 THREE.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
@@ -2439,9 +2451,7 @@ THREE.GLTFLoader = ( function () {
 
 		for ( var gltfAttributeName in attributes ) {
 
-			var threeAttributeName = ATTRIBUTES[ gltfAttributeName ];
-
-			if ( ! threeAttributeName ) continue;
+			var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase();
 
 			// Skip attributes already provided by e.g. Draco extension.
 			if ( threeAttributeName in geometry.attributes ) continue;
@@ -2478,8 +2488,6 @@ THREE.GLTFLoader = ( function () {
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
 	 *
 	 * Creates BufferGeometries from primitives.
-	 * If we can build a single BufferGeometry with .groups from multiple primitives, returns one BufferGeometry.
-	 * Otherwise, returns BufferGeometries without .groups as many as primitives.
 	 *
 	 * @param {Array<GLTF.Primitive>} primitives
 	 * @return {Promise<Array<THREE.BufferGeometry>>}
@@ -2490,22 +2498,6 @@ THREE.GLTFLoader = ( function () {
 		var extensions = this.extensions;
 		var cache = this.primitiveCache;
 
-		var isMultiPass = isMultiPassGeometry( primitives );
-		var originalPrimitives;
-
-		if ( isMultiPass ) {
-
-			originalPrimitives = primitives; // save original primitives and use later
-
-			// We build a single BufferGeometry with .groups from multiple primitives
-			// because all primitives share the same attributes/morph/mode and have indices.
-
-			primitives = [ primitives[ 0 ] ];
-
-			// Sets .groups and combined indices to a geometry later in this method.
-
-		}
-
 		function createDracoPrimitive( primitive ) {
 
 			return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
@@ -2558,97 +2550,7 @@ THREE.GLTFLoader = ( function () {
 
 		}
 
-		return Promise.all( pending ).then( function ( geometries ) {
-
-			if ( isMultiPass ) {
-
-				var baseGeometry = geometries[ 0 ];
-
-				// See if we've already created this combined geometry
-				var cache = parser.multiPassGeometryCache;
-				var cacheKey = createMultiPassGeometryKey( baseGeometry, originalPrimitives );
-				var cached = cache[ cacheKey ];
-
-				if ( cached !== null ) return [ cached.geometry ];
-
-				// Cloning geometry because of index override.
-				// Attributes can be reused so cloning by myself here.
-				var geometry = new THREE.BufferGeometry();
-
-				geometry.name = baseGeometry.name;
-				geometry.userData = baseGeometry.userData;
-
-				for ( var key in baseGeometry.attributes ) geometry.addAttribute( key, baseGeometry.attributes[ key ] );
-				for ( var key in baseGeometry.morphAttributes ) geometry.morphAttributes[ key ] = baseGeometry.morphAttributes[ key ];
-
-				var pendingIndices = [];
-
-				for ( var i = 0, il = originalPrimitives.length; i < il; i ++ ) {
-
-					pendingIndices.push( parser.getDependency( 'accessor', originalPrimitives[ i ].indices ) );
-
-				}
-
-				return Promise.all( pendingIndices ).then( function ( accessors ) {
-
-					var indices = [];
-					var offset = 0;
-
-					for ( var i = 0, il = originalPrimitives.length; i < il; i ++ ) {
-
-						var accessor = accessors[ i ];
-
-						for ( var j = 0, jl = accessor.count; j < jl; j ++ ) indices.push( accessor.array[ j ] );
-
-						geometry.addGroup( offset, accessor.count, i );
-
-						offset += accessor.count;
-
-					}
-
-					geometry.setIndex( indices );
-
-					cache[ cacheKey ] = { geometry: geometry, baseGeometry: baseGeometry, primitives: originalPrimitives };
-
-					return [ geometry ];
-
-				} );
-
-			} else if ( geometries.length > 1 && THREE.BufferGeometryUtils !== undefined ) {
-
-				// Tries to merge geometries with BufferGeometryUtils if possible
-
-				for ( var i = 1, il = primitives.length; i < il; i ++ ) {
-
-					// can't merge if draw mode is different
-					if ( primitives[ 0 ].mode !== primitives[ i ].mode ) return geometries;
-
-				}
-
-				// See if we've already created this combined geometry
-				var cache = parser.multiplePrimitivesCache;
-				var cacheKey = createArrayKeyBufferGeometry( geometries );
-				var cached = cache[ cacheKey ];
-
-				if ( cached ) {
-
-					if ( cached.geometry !== null ) return [ cached.geometry ];
-
-				} else {
-
-					var geometry = THREE.BufferGeometryUtils.mergeBufferGeometries( geometries, true );
-
-					cache[ cacheKey ] = { geometry: geometry, baseGeometries: geometries };
-
-					if ( geometry !== null ) return [ geometry ];
-
-				}
-
-			}
-
-			return geometries;
-
-		} );
+		return Promise.all( pending );
 
 	};
 
@@ -2682,8 +2584,6 @@ THREE.GLTFLoader = ( function () {
 
 			return parser.loadGeometries( primitives ).then( function ( geometries ) {
 
-				var isMultiMaterial = geometries.length === 1 && geometries[ 0 ].groups.length > 0;
-
 				var meshes = [];
 
 				for ( var i = 0, il = geometries.length; i < il; i ++ ) {
@@ -2695,7 +2595,7 @@ THREE.GLTFLoader = ( function () {
 
 					var mesh;
 
-					var material = isMultiMaterial ? originalMaterials : originalMaterials[ i ];
+					var material = originalMaterials[ i ];
 
 					if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
 						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
@@ -2753,121 +2653,9 @@ THREE.GLTFLoader = ( function () {
 
 					assignExtrasToUserData( mesh, meshDef );
 
-					meshes.push( mesh );
-
-					// 2. update Material depending on Mesh and BufferGeometry
-
-					var materials = isMultiMaterial ? mesh.material : [ mesh.material ];
-
-					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;
-
-					for ( var j = 0, jl = materials.length; j < jl; j ++ ) {
-
-						var material = materials[ j ];
-
-						if ( mesh.isPoints ) {
-
-							var cacheKey = 'PointsMaterial:' + material.uuid;
-
-							var pointsMaterial = parser.cache.get( cacheKey );
-
-							if ( ! pointsMaterial ) {
-
-								pointsMaterial = new THREE.PointsMaterial();
-								THREE.Material.prototype.copy.call( pointsMaterial, material );
-								pointsMaterial.color.copy( material.color );
-								pointsMaterial.map = material.map;
-								pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
-
-								parser.cache.add( cacheKey, pointsMaterial );
-
-							}
-
-							material = pointsMaterial;
-
-						} else if ( mesh.isLine ) {
-
-							var cacheKey = 'LineBasicMaterial:' + material.uuid;
-
-							var lineMaterial = parser.cache.get( cacheKey );
-
-							if ( ! lineMaterial ) {
-
-								lineMaterial = new THREE.LineBasicMaterial();
-								THREE.Material.prototype.copy.call( lineMaterial, material );
-								lineMaterial.color.copy( material.color );
-								lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet
-
-								parser.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 = parser.cache.get( cacheKey );
+					parser.assignFinalMaterial( mesh );
 
-							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 = THREE.VertexColors;
-								if ( useFlatShading ) cachedMaterial.flatShading = true;
-								if ( useMorphTargets ) cachedMaterial.morphTargets = true;
-								if ( useMorphNormals ) cachedMaterial.morphNormals = true;
-
-								parser.cache.add( cacheKey, cachedMaterial );
-
-							}
-
-							material = cachedMaterial;
-
-						}
-
-						materials[ j ] = material;
-
-						// 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 THREE.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 = isMultiMaterial ? materials : materials[ 0 ];
+					meshes.push( mesh );
 
 				}
 
@@ -3052,10 +2840,7 @@ THREE.GLTFLoader = ( function () {
 
 				if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {
 
-					// node can be THREE.Group here but
-					// PATH_PROPERTIES.weights(morphTargetInfluences) should be
-					// the property of a mesh object under group.
-
+					// Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh.
 					node.traverse( function ( object ) {
 
 						if ( object.isMesh === true && object.morphTargetInfluences ) {
@@ -3072,20 +2857,16 @@ THREE.GLTFLoader = ( function () {
 
 				}
 
-				// KeyframeTrack.optimize() will modify given 'times' and 'values'
-				// buffers before creating a truncated copy to keep. Because buffers may
-				// be reused by other tracks, make copies here.
 				for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {
 
 					var track = new TypedKeyframeTrack(
 						targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
-						THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ),
-						THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ),
+						inputAccessor.array,
+						outputAccessor.array,
 						interpolation
 					);
 
-					// Here is the trick to enable custom interpolation.
-					// Overrides .createInterpolant in a factory method which creates custom interpolation.
+					// Override interpolation with custom factory method.
 					if ( sampler.interpolation === 'CUBICSPLINE' ) {
 
 						track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) {
@@ -3098,8 +2879,7 @@ THREE.GLTFLoader = ( function () {
 
 						};
 
-						// Workaround, provide an alternate way to know if the interpolant type is cubis spline to track.
-						// track.getInterpolation() doesn't return valid value for custom interpolant.
+						// Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants.
 						track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;
 
 					}

+ 2310 - 0
examples/js/loaders/LWOLoader.js

@@ -0,0 +1,2310 @@
+/**
+ * @author Lewy Blue https://github.com/looeee
+ *
+ * Load files in LWO3 format
+ *
+ * LWO3 format specification:
+ * 	http://static.lightwave3d.com/sdk/2018/html/filefmts/lwo3.html
+ *
+ * LWO2 format specification (not tested, however the loader should be largely backwards compatible)
+ * 	http://static.lightwave3d.com/sdk/2018/html/filefmts/lwo2.html
+ *
+ */
+
+THREE.LWOLoader = ( function () {
+
+	var lwoTree;
+
+	function LWOLoader( manager ) {
+
+		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
+
+	}
+
+	LWOLoader.prototype = {
+
+		constructor: LWOLoader,
+
+		crossOrigin: 'anonymous',
+
+		load: function ( url, onLoad, onProgress, onError ) {
+
+			var self = this;
+
+			var path = ( self.path === undefined ) ? THREE.LoaderUtils.extractUrlBase( url ) : self.path;
+
+			// give the mesh a default name based on the filename
+			var modelName = url.split( path ).pop().split( '.' )[ 0 ];
+
+			var loader = new THREE.FileLoader( this.manager );
+			loader.setPath( self.path );
+			loader.setResponseType( 'arraybuffer' );
+
+			loader.load( url, function ( buffer ) {
+
+				// console.time( 'Total parsing: ' );
+				onLoad( self.parse( buffer, path, modelName ) );
+				// console.timeEnd( 'Total parsing: ' );
+
+			}, 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;
+
+		},
+
+		parse: function ( iffBuffer, path, modelName ) {
+
+			lwoTree = new IFFParser().parse( iffBuffer );
+
+			// console.log( 'lwoTree', lwoTree );
+
+			var textureLoader = new THREE.TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin );
+
+			return new LWOTreeParser( textureLoader ).parse( modelName );
+
+		}
+
+	};
+
+	// Parse the lwoTree object
+	function LWOTreeParser( textureLoader ) {
+
+		this.textureLoader = textureLoader;
+
+	}
+
+	LWOTreeParser.prototype = {
+
+		constructor: LWOTreeParser,
+
+		parse: function ( modelName ) {
+
+			this.materials = new MaterialParser( this.textureLoader ).parse();
+			this.defaultLayerName = modelName;
+
+			this.meshes = this.parseLayers();
+
+			return {
+				materials: this.materials,
+				meshes: this.meshes,
+			};
+
+		},
+
+		parseLayers() {
+
+			// array of all meshes for building hierarchy
+			var meshes = [];
+
+			// final array containing meshes with scene graph hierarchy set up
+			var finalMeshes = [];
+
+			var geometryParser = new GeometryParser();
+
+			var self = this;
+			lwoTree.layers.forEach( function ( layer ) {
+
+				var geometry = geometryParser.parse( layer.geometry, layer );
+
+				var mesh = self.parseMesh( geometry, layer );
+
+				meshes[ layer.number ] = mesh;
+
+				if ( layer.parent === - 1 ) finalMeshes.push( mesh );
+				else meshes[ layer.parent ].add( mesh );
+
+
+			} );
+
+			this.applyPivots( finalMeshes );
+
+			return finalMeshes;
+
+		},
+
+		parseMesh( geometry, layer ) {
+
+			var mesh;
+
+			var materials = this.getMaterials( geometry.userData.matNames, layer.geometry.type );
+
+			this.duplicateUVs( geometry, materials );
+
+			if ( layer.geometry.type === 'points' ) mesh = new THREE.Points( geometry, materials );
+			else if ( layer.geometry.type === 'lines' ) mesh = new THREE.LineSegments( geometry, materials );
+			else mesh = new THREE.Mesh( geometry, materials );
+
+			if ( layer.name ) mesh.name = layer.name;
+			else mesh.name = this.defaultLayerName + '_layer_' + layer.number;
+
+			mesh.userData.pivot = layer.pivot;
+
+			return mesh;
+
+		},
+
+		// TODO: may need to be reversed in z to convert LWO to three.js coordinates
+		applyPivots( meshes ) {
+
+			meshes.forEach( function ( mesh ) {
+
+				mesh.traverse( function ( child ) {
+
+					var pivot = child.userData.pivot;
+
+					child.position.x += pivot[ 0 ];
+					child.position.y += pivot[ 1 ];
+					child.position.z += pivot[ 2 ];
+
+					if ( child.parent ) {
+
+						var parentPivot = child.parent.userData.pivot;
+
+						child.position.x -= parentPivot[ 0 ];
+						child.position.y -= parentPivot[ 1 ];
+						child.position.z -= parentPivot[ 2 ];
+
+					}
+
+				} );
+
+			} );
+
+		},
+
+		getMaterials( namesArray, type ) {
+
+			var materials = [];
+
+			var self = this;
+
+			namesArray.forEach( function ( name, i ) {
+
+				materials[ i ] = self.getMaterialByName( name );
+
+			} );
+
+			// convert materials to line or point mats if required
+			if ( type === 'points' || type === 'lines' ) {
+
+				materials.forEach( function ( mat, i ) {
+
+					var spec = {
+						color: mat.color,
+					};
+
+					if ( type === 'points' ) {
+
+						spec.size = 0.1;
+						spec.map = mat.map;
+						spec.morphTargets = mat.morphTargets;
+						materials[ i ] = new THREE.PointsMaterial( spec );
+
+					} else if ( type === 'lines' ) {
+
+						materials[ i ] = new THREE.LineBasicMaterial( spec );
+
+					}
+
+				} );
+
+			}
+
+			// if there is only one material, return that directly instead of array
+			var filtered = materials.filter( Boolean );
+			if ( filtered.length === 1 ) return filtered[ 0 ];
+
+			return materials;
+
+		},
+
+		getMaterialByName( name ) {
+
+			return this.materials.filter( function ( m ) {
+
+				return m.name === name;
+
+			} )[ 0 ];
+
+		},
+
+		// If the material has an aoMap, duplicate UVs
+		duplicateUVs( geometry, materials ) {
+
+			var duplicateUVs = false;
+
+			if ( ! Array.isArray( materials ) ) {
+
+				if ( materials.aoMap ) duplicateUVs = true;
+
+			} else {
+
+				materials.forEach( function ( material ) {
+
+					if ( material.aoMap ) duplicateUVs = true;
+
+				} );
+
+			}
+
+			if ( ! duplicateUVs ) return;
+
+			geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
+
+		},
+
+	};
+
+	function MaterialParser( textureLoader ) {
+
+		this.textureLoader = textureLoader;
+
+	}
+
+	MaterialParser.prototype = {
+
+		constructor: MaterialParser,
+
+		parse: function () {
+
+			var materials = [];
+			this.textures = {};
+
+			for ( var name in lwoTree.materials ) {
+
+				materials.push( this.parseMaterial( lwoTree.materials[ name ], name, lwoTree.textures ) );
+
+			}
+
+			return materials;
+
+		},
+
+		parseMaterial( materialData, name, textures ) {
+
+			var params = {
+				name: name,
+				side: this.getSide( materialData.attributes ),
+				flatShading: this.getSmooth( materialData.attributes ),
+			};
+
+			var connections = this.parseConnections( materialData.connections, materialData.nodes );
+
+			var maps = this.parseTextureNodes( connections.maps );
+
+			this.parseAttributeImageMaps( connections.attributes, textures, maps, materialData.maps );
+
+			var attributes = this.parseAttributes( connections.attributes, maps );
+
+			this.parseEnvMap( connections, maps, attributes );
+
+			params = Object.assign( maps, params );
+			params = Object.assign( params, attributes );
+
+			var type = connections.attributes.Roughness ? 'Standard' : 'Phong';
+
+			return new THREE[ 'Mesh' + type + 'Material' ]( params );
+
+		},
+
+		// Note: converting from left to right handed coords by switching x -> -x in vertices, and
+		// then switching mat FrontSide -> BackSide
+		// NB: this means that THREE.FrontSide and THREE.BackSide have been switched!
+		getSide( attributes ) {
+
+			if ( ! attributes.side ) return THREE.BackSide;
+
+			switch ( attributes.side ) {
+
+				case 0:
+				case 1:
+					return THREE.BackSide;
+				case 2: return THREE.FrontSide;
+				case 3: return THREE.DoubleSide;
+
+			}
+
+		},
+
+		getSmooth( attributes ) {
+
+			if ( ! attributes.smooth ) return true;
+			return ! attributes.smooth;
+
+		},
+
+		parseConnections( connections, nodes ) {
+
+			var materialConnections = {
+				maps: {}
+			};
+
+			var inputName = connections.inputName;
+			var inputNodeName = connections.inputNodeName;
+			var nodeName = connections.nodeName;
+
+			var self = this;
+			inputName.forEach( function ( name, index ) {
+
+				if ( name === 'Material' ) {
+
+					var matNode = self.getNodeByRefName( inputNodeName[ index ], nodes );
+					materialConnections.attributes = matNode.attributes;
+					materialConnections.envMap = matNode.fileName;
+					materialConnections.name = inputNodeName[ index ];
+
+				}
+
+			} );
+
+			nodeName.forEach( function ( name, index ) {
+
+				if ( name === materialConnections.name ) {
+
+					materialConnections.maps[ inputName[ index ] ] = self.getNodeByRefName( inputNodeName[ index ], nodes );
+
+				}
+
+			} );
+
+			return materialConnections;
+
+		},
+
+		getNodeByRefName( refName, nodes ) {
+
+			for ( var name in nodes ) {
+
+				if ( nodes[ name ].refName === refName ) return nodes[ name ];
+
+			}
+
+		},
+
+		parseTextureNodes( textureNodes ) {
+
+			var maps = {};
+
+			for ( name in textureNodes ) {
+
+				var node = textureNodes[ name ];
+				var path = node.fileName;
+
+				if ( ! path ) return;
+
+				var texture = this.loadTexture( path );
+
+				if ( node.widthWrappingMode !== undefined ) texture.wrapS = this.getWrappingType( node.widthWrappingMode );
+				if ( node.heightWrappingMode !== undefined ) texture.wrapT = this.getWrappingType( node.heightWrappingMode );
+
+				switch ( name ) {
+
+					case 'Color':
+						maps.map = texture;
+						break;
+					case 'Roughness':
+						maps.roughnessMap = texture;
+						maps.roughness = 0.5;
+						break;
+					case 'Specular':
+						maps.specularMap = texture;
+						maps.specular = 0xffffff;
+						break;
+					case 'Luminous':
+						maps.emissiveMap = texture;
+						maps.emissive = 0x808080;
+						break;
+					case 'Metallic':
+						maps.metalnessMap = texture;
+						maps.metalness = 0.5;
+						break;
+					case 'Transparency':
+					case 'Alpha':
+						maps.alphaMap = texture;
+						maps.transparent = true;
+						break;
+					case 'Normal':
+						maps.normalMap = texture;
+						if ( node.amplitude !== undefined ) maps.normalScale = new THREE.Vector2( node.amplitude, node.amplitude );
+						break;
+					case 'Bump':
+						maps.bumpMap = texture;
+						break;
+
+				}
+
+			}
+
+			// LWO BSDF materials can have both spec and rough, but this is not valid in three
+			if ( maps.roughnessMap && maps.specularMap ) delete maps.specularMap;
+
+			return maps;
+
+		},
+
+		// maps can also be defined on individual material attributes, parse those here
+		// This occurs on Standard (Phong) surfaces
+		parseAttributeImageMaps( attributes, textures, maps ) {
+
+			for ( var name in attributes ) {
+
+				var attribute = attributes[ name ];
+
+				if ( attribute.maps ) {
+
+					var mapData = attribute.maps[ 0 ];
+
+					var path = this.getTexturePathByIndex( mapData.imageIndex, textures );
+					if ( ! path ) return;
+
+					var texture = this.loadTexture( path );
+
+					if ( mapData.wrap !== undefined ) texture.wrapS = this.getWrappingType( mapData.wrap.w );
+					if ( mapData.wrap !== undefined ) texture.wrapT = this.getWrappingType( mapData.wrap.h );
+
+					switch ( name ) {
+
+						case 'Color':
+							maps.map = texture;
+							break;
+						case 'Diffuse':
+							maps.aoMap = texture;
+							break;
+						case 'Roughness':
+							maps.roughnessMap = texture;
+							maps.roughness = 1;
+							break;
+						case 'Specular':
+							maps.specularMap = texture;
+							maps.specular = 0xffffff;
+							break;
+						case 'Luminosity':
+							maps.emissiveMap = texture;
+							maps.emissive = 0x808080;
+							break;
+						case 'Metallic':
+							maps.metalnessMap = texture;
+							maps.metalness = 1;
+							break;
+						case 'Transparency':
+						case 'Alpha':
+							maps.alphaMap = texture;
+							maps.transparent = true;
+							break;
+						case 'Normal':
+							maps.normalMap = texture;
+							break;
+						case 'Bump':
+							maps.bumpMap = texture;
+							break;
+
+					}
+
+				}
+
+			}
+
+		},
+
+		parseAttributes( attributes, maps ) {
+
+			var params = {};
+
+			// don't use color data if color map is present
+			if ( attributes.Color && ! maps.map ) {
+
+				params.color = new THREE.Color().fromArray( attributes.Color.value );
+
+			} else params.color = new THREE.Color();
+
+
+			if ( attributes.Transparency && attributes.Transparency.value !== 0 ) {
+
+				params.opacity = 1 - attributes.Transparency.value;
+				params.transparent = true;
+
+			}
+
+			if ( attributes[ 'Bump Height' ] ) params.bumpScale = attributes[ 'Bump Height' ].value * 0.1;
+
+			if ( attributes[ 'Refraction Index' ] ) params.refractionRatio = 1 / attributes[ 'Refraction Index' ].value;
+
+			this.parseStandardAttributes( params, attributes, maps );
+			this.parsePhongAttributes( params, attributes, maps );
+
+			return params;
+
+		},
+
+		parseStandardAttributes( params, attributes, maps ) {
+
+			if ( attributes.Luminous && attributes.Luminous.value !== 0 && attributes[ 'Luminous Color' ] ) {
+
+				var emissiveColor = attributes[ 'Luminous Color' ].value.map( function ( val ) {
+
+					return val * attributes.Luminous.value;
+
+				} );
+
+				params.emissive = new THREE.Color().fromArray( emissiveColor );
+
+			}
+			if ( attributes.Roughness && ! maps.roughnessMap ) params.roughness = attributes.Roughness.value;
+			if ( attributes.Metallic && ! maps.metalnessMap ) params.metalness = attributes.Metallic.value;
+
+		},
+
+		parsePhongAttributes( params, attributes, maps ) {
+
+			if ( attributes.Diffuse ) params.color.multiplyScalar( attributes.Diffuse.value );
+
+			if ( attributes.Reflection ) {
+
+				params.reflectivity = attributes.Reflection.value;
+				params.combine = THREE.AddOperation;
+
+			}
+
+			if ( attributes.Luminosity && ! maps.emissiveMap ) params.emissive = new THREE.Color().setScalar( attributes.Luminosity.value );
+
+			if ( attributes.Glossiness !== undefined ) params.shininess = 5 + Math.pow( attributes.Glossiness.value * 7, 6 );
+
+			// parse specular if there is no roughness - we will interpret the material as 'Phong' in this case
+			if ( ! attributes.Roughness && attributes.Specular && ! maps.specularMap ) params.specular = new THREE.Color().setScalar( attributes.Specular.value * 1.5 );
+
+		},
+
+		parseEnvMap( connections, maps, attributes ) {
+
+			if ( connections.envMap ) {
+
+				var envMap = this.loadTexture( connections.envMap );
+
+				if ( attributes.transparent && attributes.opacity < 0.999 ) {
+
+					envMap.mapping = THREE.EquirectangularRefractionMapping;
+
+					// Reflectivity and refraction mapping don't work well together in Phong materials
+					if ( attributes.reflectivity !== undefined ) {
+
+						delete attributes.reflectivity;
+						delete attributes.combine;
+
+					}
+
+					if ( attributes.metalness !== undefined ) {
+
+						delete attributes.metalness;
+
+					}
+
+				} else envMap.mapping = THREE.EquirectangularReflectionMapping;
+
+				maps.envMap = envMap;
+
+			}
+
+		},
+
+		// get texture defined at top level by its index
+		getTexturePathByIndex( index ) {
+
+			var fileName = '';
+
+			if ( ! lwoTree.textures ) return fileName;
+
+			lwoTree.textures.forEach( function ( texture ) {
+
+				if ( texture.index === index ) fileName = texture.fileName;
+
+			} );
+
+			return fileName;
+
+		},
+
+		loadTexture( path ) {
+
+			if ( ! path ) return null;
+
+			return this.textureLoader.load( this.cleanPath( path ) );
+
+		},
+
+		// Lightwave expects textures to be in folder called Images relative
+		// to the model
+		// Otherwise, the full absolute path is stored: D://some_directory/textures/bumpMap.png
+		// In this case, we'll strip out everything and load 'bumpMap.png' from the same directory as the model
+		cleanPath( path ) {
+
+			if ( path.indexOf( 'Images' ) === 0 ) return './' + path;
+			return path.split( '/' ).pop().split( '\\' ).pop();
+
+		},
+
+		// 0 = Reset, 1 = Repeat, 2 = Mirror, 3 = Edge
+		getWrappingType( num ) {
+
+			switch ( num ) {
+
+				case 0:
+					console.warn( 'LWOLoader: "Reset" texture wrapping type is not supported in three.js' );
+					return THREE.ClampToEdgeWrapping;
+				case 1: return THREE.RepeatWrapping;
+				case 2: return THREE.MirroredRepeatWrapping;
+				case 3: return THREE.ClampToEdgeWrapping;
+
+			}
+
+		},
+
+		getType( nodeData ) {
+
+			if ( nodeData.roughness ) return 'Standard';
+			return 'Phong';
+
+		},
+
+	};
+
+	function GeometryParser() {}
+
+	GeometryParser.prototype = {
+
+		constructor: GeometryParser,
+
+		parse( geoData, layer ) {
+
+			var geometry = new THREE.BufferGeometry();
+
+			geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( geoData.points, 3 ) );
+
+			var indices = this.splitIndices( geoData.vertexIndices, geoData.polygonDimensions );
+			geometry.setIndex( indices );
+
+			this.parseGroups( geometry, geoData );
+
+			geometry.computeVertexNormals();
+
+			this.parseUVs( geometry, layer, indices );
+			this.parseMorphTargets( geometry, layer, indices );
+
+			// TODO: z may need to be reversed to account for coordinate system change
+			geometry.translate( - layer.pivot[ 0 ], - layer.pivot[ 1 ], - layer.pivot[ 2 ] );
+
+			// var userData = geometry.userData;
+			// geometry = geometry.toNonIndexed()
+			// geometry.userData = userData;
+
+			return geometry;
+
+		},
+
+		// split quads into tris
+		splitIndices( indices, polygonDimensions ) {
+
+			var remappedIndices = [];
+
+			var i = 0;
+			polygonDimensions.forEach( function ( dim ) {
+
+				if ( dim < 4 ) {
+
+					for ( var k = 0; k < dim; k ++ ) remappedIndices.push( indices[ i + k ] );
+
+				} else if ( dim === 4 ) {
+
+					remappedIndices.push(
+						indices[ i ],
+						indices[ i + 1 ],
+						indices[ i + 2 ],
+
+						indices[ i ],
+						indices[ i + 2 ],
+						indices[ i + 3 ]
+
+					);
+
+				} else if ( dim > 4 ) console.warn( 'LWOLoader: polygons with greater than 4 sides are not supported' );
+
+				i += dim;
+
+			} );
+
+			return remappedIndices;
+
+		},
+
+		// NOTE: currently ignoring poly indices and assuming that they are intelligently ordered
+		parseGroups( geometry, geoData ) {
+
+			var tags = lwoTree.tags;
+			var matNames = [];
+
+			var elemSize = 3;
+			if ( geoData.type === 'lines' ) elemSize = 2;
+			if ( geoData.type === 'points' ) elemSize = 1;
+
+			var remappedIndices = this.splitMaterialIndices( geoData.polygonDimensions, geoData.materialIndices );
+
+			var indexNum = 0; // create new indices in numerical order
+			var indexPairs = {}; // original indices mapped to numerical indices
+
+			var prevMaterialIndex;
+
+			var prevStart = 0;
+			var currentCount = 0;
+
+			for ( var i = 0; i < remappedIndices.length; i += 2 ) {
+
+				var materialIndex = remappedIndices[ i + 1 ];
+
+				if ( i === 0 ) matNames[ indexNum ] = tags[ materialIndex ];
+
+				if ( prevMaterialIndex === undefined ) prevMaterialIndex = materialIndex;
+
+				if ( materialIndex !== prevMaterialIndex ) {
+
+					var currentIndex;
+					if ( indexPairs[ tags[ prevMaterialIndex ] ] ) {
+
+						currentIndex = indexPairs[ tags[ prevMaterialIndex ] ];
+
+					} else {
+
+						currentIndex = indexNum;
+						indexPairs[ tags[ prevMaterialIndex ] ] = indexNum;
+						matNames[ indexNum ] = tags[ prevMaterialIndex ];
+						indexNum ++;
+
+					}
+
+					geometry.addGroup( prevStart, currentCount, currentIndex );
+
+					prevStart += currentCount;
+
+					prevMaterialIndex = materialIndex;
+					currentCount = 0;
+
+				}
+
+				currentCount += elemSize;
+
+			}
+
+			// the loop above doesn't add the last group, do that here.
+			if ( geometry.groups.length > 0 ) {
+
+				var currentIndex;
+				if ( indexPairs[ tags[ materialIndex ] ] ) {
+
+					currentIndex = indexPairs[ tags[ materialIndex ] ];
+
+				} else {
+
+					currentIndex = indexNum;
+					indexPairs[ tags[ materialIndex ] ] = indexNum;
+					matNames[ indexNum ] = tags[ materialIndex ];
+
+				}
+
+				geometry.addGroup( prevStart, currentCount, currentIndex );
+
+			}
+
+			// Mat names from TAGS chunk, used to build up an array of materials for this geometry
+			geometry.userData.matNames = matNames;
+
+		},
+
+		splitMaterialIndices( polygonDimensions, indices ) {
+
+			var remappedIndices = [];
+
+			polygonDimensions.forEach( function ( dim, i ) {
+
+				if ( dim <= 3 ) {
+
+					remappedIndices.push( indices[ i * 2 ], indices[ i * 2 + 1 ] );
+
+				} else if ( dim === 4 ) {
+
+					remappedIndices.push( indices[ i * 2 ], indices[ i * 2 + 1 ], indices[ i * 2 ], indices[ i * 2 + 1 ] );
+
+				} // ignore > 4 for now
+
+			} );
+
+			return remappedIndices;
+
+		},
+
+		// UV maps:
+		// 1: are defined via index into an array of points, not into a geometry
+		// - the geometry is also defined by an index into this array, but the indexes may not match
+		// 2: there can be any number of UV maps for a single geometry. Here these are combined,
+		// 	with preference given to the first map encountered
+		// 3: UV maps can be partial - that is, defined for only a part of the geometry
+		// 4: UV maps can be VMAP or VMAD (discontinuous, to allow for seams). In practice, most
+		// UV maps are defined as partially VMAP and partially VMAD
+		// VMADs are currently not supported
+		parseUVs( geometry, layer ) {
+
+			// start by creating a UV map set to zero for the whole geometry
+			var remappedUVs = Array.from( Array( geometry.attributes.position.count * 2 ), function () {
+
+				return 0;
+
+			} );
+
+			for ( var name in layer.uvs ) {
+
+				var uvs = layer.uvs[ name ].uvs;
+				var uvIndices = layer.uvs[ name ].uvIndices;
+
+				uvIndices.forEach( function ( i, j ) {
+
+					remappedUVs[ i * 2 ] = uvs[ j * 2 ];
+					remappedUVs[ i * 2 + 1 ] = uvs[ j * 2 + 1 ];
+
+				} );
+
+			}
+
+			geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( remappedUVs, 2 ) );
+
+		},
+
+		parseMorphTargets( geometry, layer ) {
+
+			var num = 0;
+			for ( var name in layer.morphTargets ) {
+
+				var remappedPoints = geometry.attributes.position.array.slice();
+
+				if ( ! geometry.morphAttributes.position ) geometry.morphAttributes.position = [];
+
+				var morphPoints = layer.morphTargets[ name ].points;
+				var morphIndices = layer.morphTargets[ name ].indices;
+				var type = layer.morphTargets[ name ].type;
+
+				morphIndices.forEach( function ( i, j ) {
+
+					if ( type === 'relative' ) {
+
+						remappedPoints[ i * 3 ] += morphPoints[ j * 3 ];
+						remappedPoints[ i * 3 + 1 ] += morphPoints[ j * 3 + 1 ];
+						remappedPoints[ i * 3 + 2 ] += morphPoints[ j * 3 + 2 ];
+
+					} else {
+
+						remappedPoints[ i * 3 ] = morphPoints[ j * 3 ];
+						remappedPoints[ i * 3 + 1 ] = morphPoints[ j * 3 + 1 ];
+						remappedPoints[ i * 3 + 2 ] = morphPoints[ j * 3 + 2 ];
+
+					}
+
+				} );
+
+				geometry.morphAttributes.position[ num ] = new THREE.Float32BufferAttribute( remappedPoints, 3 );
+				geometry.morphAttributes.position[ num ].name = name;
+
+				num ++;
+
+			}
+
+		},
+
+	};
+
+	// parse data from the IFF buffer.
+	// LWO3 files are in IFF format and can contain the following data types, referred to by shorthand codes
+	//
+	// ATOMIC DATA TYPES
+	// ID Tag - 4x 7 bit uppercase ASCII chars: ID4
+	// signed integer, 1, 2, or 4 byte length: I1, I2, I4
+	// unsigned integer, 1, 2, or 4 byte length: U1, U2, U4
+	// float, 4 byte length: F4
+	// string, series of ASCII chars followed by null byte (If the length of the string including the null terminating byte is odd, an extra null is added so that the data that follows will begin on an even byte boundary): S0
+	//
+	//  COMPOUND DATA TYPES
+	// Variable-length Index (index into an array or collection): U2 or U4 : VX
+	// Color (RGB): F4 + F4 + F4: COL12
+	// Coordinate (x, y, z): F4 + F4 + F4: VEC12
+	// Percentage F4 data type from 0->1 with 1 = 100%: FP4
+	// Angle in radian F4: ANG4
+	// Filename (string) S0: FNAM0
+	// XValue F4 + index (VX) + optional envelope( ENVL ): XVAL
+	// XValue vector VEC12 + index (VX) + optional envelope( ENVL ): XVAL3
+	//
+	// The IFF file is arranged in chunks:
+	// CHUNK = ID4 + length (U4) + length X bytes of data + optional 0 pad byte
+	// optional 0 pad byte is there to ensure chunk ends on even boundary, not counted in size
+
+	// Chunks are combined in Forms (collections of chunks)
+	// FORM = string 'FORM' (ID4) + length (U4) + type (ID4) + optional ( CHUNK | FORM )
+
+	// CHUNKS and FORMS are collectively referred to as blocks
+
+	// The entire file is contained in one top level FORM
+	function IFFParser() {}
+
+	IFFParser.prototype = {
+
+		constructor: IFFParser,
+
+		parse: function ( buffer ) {
+
+			// dump the whole buffer as a string for testing
+			// printBuffer( buffer );
+
+			this.reader = new DataViewReader( buffer );
+
+			this.tree = {
+				materials: {},
+				layers: [],
+				tags: [],
+				textures: [],
+			};
+
+			// start out at the top level to add any data before first layer is encountered
+			this.currentLayer = this.tree;
+			this.currentForm = this.tree;
+
+			// parse blocks until end of file is reached
+			while ( ! this.reader.endOfFile() ) this.parseBlock();
+
+			return this.tree;
+
+		},
+
+		parseBlock() {
+
+			var blockID = this.reader.getIDTag();
+			var length = this.reader.getUint32(); // size of data in bytes
+
+			// Data types may be found in either LWO2 OR LWO3 spec
+			switch ( blockID ) {
+
+				case 'FORM': // form blocks may consist of sub -chunks or sub-forms
+					this.parseForm( length );
+					break;
+
+					// SKIPPED CHUNKS
+
+				// MISC skipped
+				case 'ICON': // Thumbnail Icon Image
+				case 'VMPA': // Vertex Map Parameter
+				case 'BBOX': // bounding box
+				// case 'VMMD':
+				// case 'VTYP':
+
+				// normal maps can be specified, normally on models imported from other applications. Currently ignored
+				case 'NORM':
+
+				// ENVL FORM skipped
+				case 'PRE ':
+				case 'POST':
+				case 'KEY ':
+				case 'SPAN':
+
+				// CLIP FORM skipped
+				case 'TIME':
+				case 'CLRS':
+				case 'CLRA':
+				case 'FILT':
+				case 'DITH':
+				case 'CONT':
+				case 'BRIT':
+				case 'SATR':
+				case 'HUE ':
+				case 'GAMM':
+				case 'NEGA':
+				case 'IFLT':
+				case 'PFLT':
+
+				// Image Map Layer skipped
+				case 'PROJ':
+				case 'AXIS':
+				case 'AAST':
+				case 'PIXB':
+				case 'STCK':
+
+				// Procedural Textures skipped
+				case 'VALU':
+
+				// Gradient Textures skipped
+				case 'PNAM':
+				case 'INAM':
+				case 'GRST':
+				case 'GREN':
+				case 'GRPT':
+				case 'FKEY':
+				case 'IKEY':
+
+				// Texture Mapping Form skipped
+				case 'CSYS':
+
+					// Surface CHUNKs skipped
+				case 'OPAQ': // top level 'opacity' checkbox
+				case 'CMAP': // clip map
+
+				// Surface node CHUNKS skipped
+				// These mainly specify the node editor setup in LW
+				case 'NLOC':
+				case 'NZOM':
+				case 'NVER':
+				case 'NSRV':
+				case 'NCRD':
+				case 'NMOD':
+				case 'NPRW':
+				case 'NPLA':
+				case 'VERS':
+				case 'ENUM':
+				case 'FLAG':
+				case 'TAG ':
+
+				// Car Material CHUNKS
+				case 'CGMD':
+				case 'CGTY':
+				case 'CGST':
+				case 'CGEN':
+				case 'CGTS':
+				case 'CGTE':
+				case 'OSMP':
+				case 'OMDE':
+				case 'OUTR':
+					this.reader.skip( length );
+					break;
+
+				// Skipped LWO2 chunks
+				case 'DIFF': // diffuse level, may be necessary to modulate COLR with this
+				case 'TRNL':
+				case 'REFL':
+				case 'GLOS':
+				case 'SHRP':
+				case 'RFOP':
+				case 'RSAN':
+				case 'TROP':
+				case 'RBLR':
+				case 'TBLR':
+				case 'CLRH':
+				case 'CLRF':
+				case 'ADTR':
+				case 'GLOW':
+				case 'LINE':
+				case 'ALPH':
+				case 'LINE':
+				case 'VCOL':
+				case 'ENAB':
+					this.reader.skip( length );
+					break;
+
+				// Texture node chunks (not in spec)
+				case 'IPIX': // usePixelBlending
+				case 'IMIP': // useMipMaps
+				case 'IMOD': // imageBlendingMode
+				case 'AMOD': // unknown
+				case 'IINV': // imageInvertAlpha
+				case 'INCR': // imageInvertColor
+				case 'IAXS': // imageAxis ( for non-UV maps)
+				case 'IFOT': // imageFallofType
+				case 'ITIM': // timing for animated textures
+				case 'IWRL':
+				case 'IUTI':
+				case 'IINX':
+				case 'IINY':
+				case 'IINZ':
+				case 'IREF': // possibly a VX for reused texture nodes
+					if ( length === 4 ) this.currentNode[ blockID ] = this.reader.getInt32();
+					else this.reader.skip( length );
+					break;
+
+				case 'OTAG':
+					this.parseObjectTag();
+					break;
+
+				case 'LAYR':
+					this.parseLayer( length );
+					break;
+
+				case 'PNTS':
+					this.parsePoints( length );
+					break;
+
+				case 'VMAP':
+					this.parseVertexMapping( length );
+					break;
+
+				case 'POLS':
+					this.parsePolygonList( length );
+					break;
+
+				case 'TAGS':
+					this.parseTagStrings( length );
+					break;
+
+				case 'PTAG':
+					this.parsePolygonTagMapping( length );
+					break;
+
+				case 'VMAD':
+					this.parseVertexMapping( length, true );
+					break;
+
+				// Misc CHUNKS
+				case 'DESC': // Description Line
+					this.currentForm.description = this.reader.getString();
+					break;
+
+				case 'TEXT':
+				case 'CMNT':
+				case 'NCOM':
+					this.currentForm.comment = this.reader.getString();
+					break;
+
+					// Envelope Form
+				case 'NAME':
+					this.currentForm.channelName = this.reader.getString();
+					break;
+
+					// Image Map Layer
+
+				case 'WRAP':
+					this.currentForm.wrap = { w: this.reader.getUint16(), h: this.reader.getUint16() };
+					break;
+
+				case 'IMAG':
+					var index = this.reader.getVariableLengthIndex();
+					this.currentForm.imageIndex = index;
+					break;
+
+					// Texture Mapping Form
+
+				case 'OREF':
+					this.currentForm.referenceObject = this.reader.getString();
+					break;
+
+				case 'ROID':
+					this.currentForm.referenceObjectID = this.reader.getUint32();
+					break;
+
+					// Surface Blocks
+
+				case 'SSHN':
+					this.currentSurface.surfaceShaderName = this.reader.getString();
+					break;
+
+				case 'AOVN':
+					this.currentSurface.surfaceCustomAOVName = this.reader.getString();
+					break;
+
+					// Nodal Blocks
+
+				case 'NSTA':
+					this.currentForm.disabled = this.reader.getUint16();
+					break;
+
+				case 'NRNM':
+					this.currentForm.realName = this.reader.getString();
+					break;
+
+				case 'NNME':
+					this.currentForm.refName = this.reader.getString();
+					this.currentSurface.nodes[ this.currentForm.refName ] = this.currentForm;
+					break;
+
+				// Nodal Blocks : connections
+				case 'INME':
+					if ( ! this.currentForm.nodeName ) this.currentForm.nodeName = [];
+					this.currentForm.nodeName.push( this.reader.getString() );
+					break;
+
+				case 'IINN':
+					if ( ! this.currentForm.inputNodeName ) this.currentForm.inputNodeName = [];
+					this.currentForm.inputNodeName.push( this.reader.getString() );
+					break;
+
+				case 'IINM':
+					if ( ! this.currentForm.inputName ) this.currentForm.inputName = [];
+					this.currentForm.inputName.push( this.reader.getString() );
+					break;
+
+				case 'IONM':
+					if ( ! this.currentForm.inputOutputName ) this.currentForm.inputOutputName = [];
+					this.currentForm.inputOutputName.push( this.reader.getString() );
+					break;
+
+				case 'FNAM':
+					this.currentForm.fileName = this.reader.getString();
+					break;
+
+				case 'CHAN': // NOTE: ENVL Forms may also have CHAN chunk, however ENVL is currently ignored
+					if ( length === 4 ) this.currentForm.textureChannel = this.reader.getIDTag();
+					else this.reader.skip( length );
+					break;
+
+					// LWO2 Spec chunks: these are needed since the SURF FORMs are often in LWO2 format
+
+				case 'SMAN':
+					var maxSmoothingAngle = this.reader.getFloat32();
+					this.currentSurface.attributes.smooth = ( maxSmoothingAngle < 0 ) ? false : true;
+					break;
+
+				case 'ENAB':
+					this.currentForm.enabled = this.reader.getUint16();
+					break;
+
+				// LWO2: Basic Surface Parameters
+				case 'COLR':
+					this.currentSurface.attributes.color = this.reader.getFloat32Array( 3 );
+					this.reader.skip( 2 ); // VX: envelope
+					break;
+
+				case 'LUMI':
+					this.currentSurface.attributes.luminosityLevel = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'SPEC':
+					this.currentSurface.attributes.specularLevel = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'REFL':
+					this.currentSurface.attributes.reflectivity = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'TRAN':
+					this.currentSurface.attributes.opacity = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'BUMP':
+					this.currentSurface.attributes.bumpStrength = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'SIDE':
+					this.currentSurface.attributes.side = this.reader.getUint16();
+					break;
+
+				case 'RIMG':
+					this.currentSurface.attributes.reflectionMap = this.reader.getVariableLengthIndex();
+					break;
+
+				case 'RIND':
+					this.currentSurface.attributes.refractiveIndex = this.reader.getFloat32();
+					this.reader.skip( 2 );
+					break;
+
+				case 'TIMG':
+					this.currentSurface.attributes.refractionMap = this.reader.getVariableLengthIndex();
+					break;
+
+				case 'IMAP':
+					this.currentSurface.attributes.imageMapIndex = this.reader.getUint32();
+					break;
+
+				case 'IUVI': // uv channel name
+					this.currentNode.UVChannel = this.reader.getString( length );
+					break;
+
+				case 'IUTL': // widthWrappingMode: 0 = Reset, 1 = Repeat, 2 = Mirror, 3 = Edge
+					this.currentNode.widthWrappingMode = this.reader.getUint32();
+					break;
+				case 'IVTL': // heightWrappingMode
+					this.currentNode.heightWrappingMode = this.reader.getUint32();
+					break;
+
+				default:
+					this.parseUnknownCHUNK( blockID, length );
+
+			}
+
+			if ( this.reader.offset >= this.currentFormEnd ) {
+
+				this.currentForm = this.parentForm;
+
+			}
+
+		},
+
+
+		///
+		// FORM PARSING METHODS
+		///
+
+		// Forms are organisational and can contain any number of sub chunks and sub forms
+		// FORM ::= 'FORM'[ID4], length[U4], type[ID4], ( chunk[CHUNK] | form[FORM] ) * }
+		parseForm( length ) {
+
+			var type = this.reader.getIDTag();
+
+			switch ( type ) {
+
+				// SKIPPED FORMS
+				// if skipForm( length ) is called, the entire form and any sub forms and chunks are skipped
+
+				case 'ISEQ': // Image sequence
+				case 'ANIM': // plug in animation
+				case 'STCC': // Color-cycling Still
+				case 'VPVL':
+				case 'VPRM':
+				case 'NROT':
+				case 'WRPW': // image wrap w ( for cylindrical and spherical projections)
+				case 'WRPH': // image wrap h
+				case 'FUNC':
+				case 'FALL':
+				case 'OPAC':
+				case 'GRAD': // gradient texture
+				case 'ENVS':
+				case 'VMOP':
+				case 'VMBG':
+
+				// Car Material FORMS
+				case 'OMAX':
+				case 'STEX':
+				case 'CKBG':
+				case 'CKEY':
+				case 'VMLA':
+				case 'VMLB':
+					this.skipForm( length ); // not currently supported
+					break;
+
+				// if break; is called directly, the position in the lwoTree is not created
+				// any sub chunks and forms are added to the parent form instead
+				case 'META':
+				case 'NNDS':
+				case 'NODS':
+				case 'NDTA':
+				case 'ADAT':
+				case 'AOVS':
+				case 'BLOK':
+
+				// used by texture nodes
+				case 'IBGC': // imageBackgroundColor
+				case 'IOPC': // imageOpacity
+				case 'IIMG': // hold reference to image path
+				case 'TXTR':
+					// this.setupForm( type, length );
+					break;
+
+				case 'IFAL': // imageFallof
+				case 'ISCL': // imageScale
+				case 'IPOS': // imagePosition
+				case 'IROT': // imageRotation
+				case 'IBMP':
+				case 'IUTD':
+				case 'IVTD':
+					this.parseTextureNodeAttribute( type );
+					break;
+
+				case 'LWO3':
+					this.tree.format = type;
+					break;
+
+				case 'ENVL':
+					this.parseEnvelope( length );
+					break;
+
+					// CLIP FORM AND SUB FORMS
+
+				case 'CLIP':
+					this.parseClip( length );
+					break;
+
+				case 'STIL':
+					this.parseImage();
+					break;
+
+				case 'XREF': // clone of another STIL
+					this.reader.skip( 8 ); // unknown
+					this.currentForm.referenceTexture = {
+						index: this.reader.getUint32(),
+						refName: this.reader.getString() // internal unique ref
+					};
+					break;
+
+					// Not in spec, used by texture nodes
+
+				case 'IMST':
+					this.parseImageStateForm( length );
+					break;
+
+					// SURF FORM AND SUB FORMS
+
+				case 'SURF':
+					this.parseSurfaceForm( length );
+					break;
+
+				case 'VALU': // Not in spec
+					this.parseValueForm( length );
+					break;
+
+				case 'NTAG':
+					this.parseSubNode( length );
+					break;
+
+				case 'NNDS':
+					this.setupForm( 'nodes', length );
+					break;
+
+				case 'ATTR': // BSDF Node Attributes
+				case 'SATR': // Standard Node Attributes
+					this.setupForm( 'attributes', length );
+					break;
+
+				case 'NCON':
+					this.parseConnections( length );
+					break;
+
+				case 'SSHA':
+					this.parentForm = this.currentForm;
+					this.currentForm = this.currentSurface;
+					this.setupForm( 'surfaceShader', length );
+					break;
+
+				case 'SSHD':
+					this.setupForm( 'surfaceShaderData', length );
+					break;
+
+				case 'ENTR': // Not in spec
+					this.parseEntryForm( length );
+					break;
+
+					// Image Map Layer
+
+				case 'IMAP':
+					this.parseImageMap( length );
+					break;
+
+				case 'TAMP':
+					this.parseXVAL( 'amplitude', length );
+					break;
+
+					//Texture Mapping Form
+
+				case 'TMAP':
+					this.setupForm( 'textureMap', length );
+					break;
+
+				case 'CNTR':
+					this.parseXVAL3( 'center', length );
+					break;
+
+				case 'SIZE':
+					this.parseXVAL3( 'scale', length );
+					break;
+
+				case 'ROTA':
+					this.parseXVAL3( 'rotation', length );
+					break;
+
+				default:
+					this.parseUnknownForm( type, length );
+
+			}
+
+		},
+
+		setupForm( type, length ) {
+
+			if ( ! this.currentForm ) this.currentForm = this.currentNode;
+
+			this.currentFormEnd = this.reader.offset + length;
+			this.parentForm = this.currentForm;
+
+			if ( ! this.currentForm[ type ] ) {
+
+				this.currentForm[ type ] = {};
+				this.currentForm = this.currentForm[ type ];
+
+
+			} else {
+
+				// should never see this unless there's a bug in the reader
+				console.warn( 'LWOLoader: form already exists on parent: ', type, this.currentForm );
+
+				this.currentForm = this.currentForm[ type ];
+
+			}
+
+
+		},
+
+		skipForm( length ) {
+
+			this.reader.skip( length - 4 );
+
+		},
+
+		parseUnknownForm( type, length ) {
+
+			console.warn( 'LWOLoader: unknown FORM encountered: ' + type, length );
+
+			printBuffer( this.reader.dv.buffer, this.reader.offset, length - 4 );
+			this.reader.skip( length - 4 );
+
+		},
+
+		parseSurfaceForm( length ) {
+
+			this.reader.skip( 8 ); // unknown Uint32 x2
+
+			var name = this.reader.getString();
+
+			var surface = {
+				attributes: {}, // LWO2 style non-node attributes will go here
+				connections: {},
+				name: name,
+				nodes: {},
+				source: this.reader.getString(),
+			};
+
+			this.tree.materials[ name ] = surface;
+			this.currentSurface = surface;
+
+			this.parentForm = this.tree.materials;
+			this.currentForm = surface;
+			this.currentFormEnd = this.reader.offset + length;
+
+		},
+
+		parseSubNode( length ) {
+
+			// parse the NRNM CHUNK of the subnode FORM to get
+			// a meaningful name for the subNode
+			// some subnodes can be renamed, but Input and Surface cannot
+
+			this.reader.skip( 8 ); // NRNM + length
+			var name = this.reader.getString();
+
+			var node = {
+				name: name
+			};
+			this.currentForm = node;
+			this.currentNode = node;
+
+			this.currentFormEnd = this.reader.offset + length;
+
+
+		},
+
+		// collect attributes from all nodes at the top level of a surface
+		parseConnections( length ) {
+
+			this.currentFormEnd = this.reader.offset + length;
+			this.parentForm = this.currentForm;
+
+			this.currentForm = this.currentSurface.connections;
+
+		},
+
+		// surface node attribute data, e.g. specular, roughness etc
+		parseEntryForm( length ) {
+
+			this.reader.skip( 8 ); // NAME + length
+			var name = this.reader.getString();
+			this.currentForm = this.currentNode.attributes;
+
+			this.setupForm( name, length );
+
+		},
+
+		// parse values from material - doesn't match up to other LWO3 data types
+		// sub form of entry form
+		parseValueForm() {
+
+			this.reader.skip( 8 ); // unknown + length
+
+			var valueType = this.reader.getString();
+
+			if ( valueType === 'double' ) {
+
+				this.currentForm.value = this.reader.getUint64();
+
+			} else if ( valueType === 'int' ) {
+
+				this.currentForm.value = this.reader.getUint32();
+
+			} else if ( valueType === 'vparam' ) {
+
+				this.reader.skip( 24 );
+				this.currentForm.value = this.reader.getFloat64();
+
+			} else if ( valueType === 'vparam3' ) {
+
+				this.reader.skip( 24 );
+				this.currentForm.value = this.reader.getFloat64Array( 3 );
+
+
+			}
+
+		},
+
+		// holds various data about texture node image state
+		// Data other thanmipMapLevel unknown
+		parseImageStateForm() {
+
+			this.reader.skip( 8 ); // unknown
+
+			this.currentForm.mipMapLevel = this.reader.getFloat32();
+
+		},
+
+		// LWO2 style image data node OR LWO3 textures defined at top level in editor (not as SURF node)
+		parseImageMap( length ) {
+
+			this.currentFormEnd = this.reader.offset + length;
+			this.parentForm = this.currentForm;
+
+			if ( ! this.currentForm.maps ) this.currentForm.maps = [];
+
+			var map = {};
+			this.currentForm.maps.push( map );
+			this.currentForm = map;
+
+			this.reader.skip( 10 ); // unknown, could be an issue if it contains a VX
+
+		},
+
+		parseTextureNodeAttribute( type ) {
+
+			this.reader.skip( 28 ); // FORM + length + VPRM + unknown + Uint32 x2 + float32
+
+			this.reader.skip( 20 ); // FORM + length + VPVL + float32 + Uint32
+
+			switch ( type ) {
+
+				case 'ISCL':
+					this.currentNode.scale = this.reader.getFloat32Array( 3 );
+					break;
+				case 'IPOS':
+					this.currentNode.position = this.reader.getFloat32Array( 3 );
+					break;
+				case 'IROT':
+					this.currentNode.rotation = this.reader.getFloat32Array( 3 );
+					break;
+				case 'IFAL':
+					this.currentNode.falloff = this.reader.getFloat32Array( 3 );
+					break;
+
+				case 'IBMP':
+					this.currentNode.amplitude = this.reader.getFloat32();
+					break;
+				case 'IUTD':
+					this.currentNode.uTiles = this.reader.getFloat32();
+					break;
+				case 'IVTD':
+					this.currentNode.vTiles = this.reader.getFloat32();
+					break;
+
+			}
+
+			this.reader.skip( 2 ); // unknown
+
+
+		},
+
+		// ENVL forms are currently ignored
+		parseEnvelope( length ) {
+
+			this.reader.skip( length - 4 ); // skipping  entirely for now
+
+		},
+
+		///
+		// CHUNK PARSING METHODS
+		///
+
+		// clips can either be defined inside a surface node, or at the top
+		// level and they have a different format in each case
+		parseClip( length ) {
+
+			var tag = this.reader.getIDTag();
+
+			// inside surface node
+			if ( tag === 'FORM' ) {
+
+				this.reader.skip( 16 );
+
+				this.currentNode.fileName = this.reader.getString();
+
+				return;
+
+			}
+
+			// otherwise top level
+			this.reader.setOffset( this.reader.offset - 4 );
+
+			this.currentFormEnd = this.reader.offset + length;
+			this.parentForm = this.currentForm;
+
+			this.reader.skip( 8 ); // unknown
+
+			var texture = {
+				index: this.reader.getUint32()
+			};
+			this.tree.textures.push( texture );
+			this.currentForm = texture;
+
+		},
+
+		parseImage() {
+
+			this.reader.skip( 8 ); // unknown
+			this.currentForm.fileName = this.reader.getString();
+
+		},
+
+		parseXVAL( type, length ) {
+
+			var endOffset = this.reader.offset + length - 4;
+			this.reader.skip( 8 );
+
+			this.currentForm[ type ] = this.reader.getFloat32();
+
+			this.reader.setOffset( endOffset ); // set end offset directly to skip optional envelope
+
+		},
+
+		parseXVAL3( type, length ) {
+
+			var endOffset = this.reader.offset + length - 4;
+			this.reader.skip( 8 );
+
+			this.currentForm[ type ] = {
+				x: this.reader.getFloat32(),
+				y: this.reader.getFloat32(),
+				z: this.reader.getFloat32(),
+			};
+
+			this.reader.setOffset( endOffset );
+
+		},
+
+		// Tags associated with an object
+		// OTAG { type[ID4], tag-string[S0] }
+		parseObjectTag() {
+
+			if ( ! this.tree.objectTags ) this.tree.objectTags = {};
+
+			this.tree.objectTags[ this.reader.getIDTag() ] = {
+				tagString: this.reader.getString()
+			};
+
+		},
+
+		// Signals the start of a new layer. All the data chunks which follow will be included in this layer until another layer chunk is encountered.
+		// LAYR: number[U2], flags[U2], pivot[VEC12], name[S0], parent[U2]
+		parseLayer( length ) {
+
+			var layer = {
+				number: this.reader.getUint16(),
+				flags: this.reader.getUint16(), // If the least significant bit of flags is set, the layer is hidden.
+				pivot: this.reader.getFloat32Array( 3 ), // Note: this seems to be superflous, as the geometry is translated when pivot is present
+				name: this.reader.getString(),
+			};
+
+			this.tree.layers.push( layer );
+			this.currentLayer = layer;
+
+			var parsedLength = 16 + stringOffset( this.currentLayer.name ); // index ( 2 ) + flags( 2 ) + pivot( 12 ) + stringlength
+
+			// if we have not reached then end of the layer block, there must be a parent defined
+			this.currentLayer.parent = ( parsedLength < length ) ? this.reader.getUint16() : - 1; // omitted or -1 for no parent
+
+		},
+
+		// VEC12 * ( F4 + F4 + F4 ) array of x,y,z vectors
+		// Converting from left to right handed coordinate system:
+		// x -> -x and switch material FrontSide -> BackSide
+		parsePoints( length ) {
+
+			this.currentPoints = [];
+			for ( var i = 0; i < length / 4; i += 3 ) {
+
+				// z -> -z to match three.js right handed coords
+				this.currentPoints.push( this.reader.getFloat32(), this.reader.getFloat32(), - this.reader.getFloat32() );
+
+			}
+
+		},
+
+		// parse VMAP or VMAD
+		// Associates a set of floating-point vectors with a set of points.
+		// VMAP: { type[ID4], dimension[U2], name[S0], ( vert[VX], value[F4] # dimension ) * }
+
+		// VMAD Associates a set of floating-point vectors with the vertices of specific polygons.
+		// Similar to VMAP UVs, but associates with polygon vertices rather than points
+		// to solve to problem of UV seams:  VMAD chunks are paired with VMAPs of the same name,
+		// if they exist. The vector values in the VMAD will then replace those in the
+		// corresponding VMAP, but only for calculations involving the specified polygons.
+		// VMAD { type[ID4], dimension[U2], name[S0], ( vert[VX], poly[VX], value[F4] # dimension ) * }
+		parseVertexMapping( length, discontinuous ) {
+
+			var finalOffset = this.reader.offset + length;
+
+			var channelName = this.reader.getString();
+
+			if ( this.reader.offset === finalOffset ) {
+
+				// then we are in a texture node and the VMAP chunk is just a reference to a UV channel name
+				this.currentForm.UVChannel = channelName;
+				return;
+
+			}
+
+			// otherwise reset to initial length and parse normal VMAP CHUNK
+			this.reader.setOffset( this.reader.offset - stringOffset( channelName ) );
+
+			var type = this.reader.getIDTag();
+
+			this.reader.getUint16(); // dimension
+			var name = this.reader.getString();
+
+			var remainingLength = length - 6 - stringOffset( name );
+
+			switch ( type ) {
+
+				case 'TXUV':
+					this.parseUVMapping( name, finalOffset, discontinuous );
+					break;
+				case 'MORF':
+				case 'SPOT':
+					this.parseMorphTargets( name, finalOffset, type ); // can't be discontinuous
+					break;
+				// unsupported VMAPs
+				case 'APSL':
+				case 'NORM':
+				case 'WGHT':
+				case 'MNVW':
+				case 'PICK':
+				case 'RGB ':
+				case 'RGBA':
+					this.reader.skip( remainingLength );
+					break;
+				default:
+					console.warn( 'LWOLoader: unknown vertex map type: ' + type );
+					this.reader.skip( remainingLength );
+
+			}
+
+		},
+
+		parseUVMapping( name, finalOffset, discontinuous ) {
+
+			var uvIndices = [];
+			var polyIndices = [];
+			var uvs = [];
+
+			while ( this.reader.offset < finalOffset ) {
+
+				uvIndices.push( this.reader.getVariableLengthIndex() );
+
+				if ( discontinuous ) polyIndices.push( this.reader.getVariableLengthIndex() );
+
+				uvs.push( this.reader.getFloat32(), this.reader.getFloat32() );
+
+			}
+
+			if ( discontinuous ) {
+
+				if ( ! this.currentLayer.discontinuousUVs ) this.currentLayer.discontinuousUVs = {};
+
+				this.currentLayer.discontinuousUVs[ name ] = {
+					uvIndices: uvIndices,
+					polyIndices: polyIndices,
+					uvs: uvs,
+				};
+
+			} else {
+
+				if ( ! this.currentLayer.uvs ) this.currentLayer.uvs = {};
+
+				this.currentLayer.uvs[ name ] = {
+					uvIndices: uvIndices,
+					uvs: uvs,
+				};
+
+			}
+
+		},
+
+		parseMorphTargets( name, finalOffset, type ) {
+
+			var indices = [];
+			var points = [];
+
+			type = ( type === 'MORF' ) ? 'relative' : 'absolute';
+
+			while ( this.reader.offset < finalOffset ) {
+
+				indices.push( this.reader.getVariableLengthIndex() );
+				// z -> -z to match three.js right handed coords
+				points.push( this.reader.getFloat32(), this.reader.getFloat32(), - this.reader.getFloat32() );
+
+			}
+
+			if ( ! this.currentLayer.morphTargets ) this.currentLayer.morphTargets = {};
+
+			this.currentLayer.morphTargets[ name ] = {
+				indices: indices,
+				points: points,
+				type: type,
+			};
+
+		},
+
+		// A list of polygons for the current layer.
+		// POLS { type[ID4], ( numvert+flags[U2], vert[VX] # numvert ) * }
+		parsePolygonList( length ) {
+
+			var finalOffset = this.reader.offset + length;
+			var type = this.reader.getIDTag();
+
+			var indices = [];
+
+			// hold a list of polygon sizes, to be split up later
+			var polygonDimensions = [];
+
+			while ( this.reader.offset < finalOffset ) {
+
+				var numverts = this.reader.getUint16();
+
+				//var flags = numverts & 64512; // 6 high order bits are flags - ignoring for now
+				numverts = numverts & 1023; // remaining ten low order bits are vertex num
+				polygonDimensions.push( numverts );
+
+				for ( var j = 0; j < numverts; j ++ ) indices.push( this.reader.getVariableLengthIndex() );
+
+			}
+
+			var geometryData = {
+				type: type,
+				vertexIndices: indices,
+				polygonDimensions: polygonDimensions,
+				points: this.currentPoints
+			};
+
+			// Note: assuming that all polys will be lines or points if the first is
+			if ( polygonDimensions[ 0 ] === 1 ) geometryData.type = 'points';
+			else if ( polygonDimensions[ 0 ] === 2 ) geometryData.type = 'lines';
+
+			this.currentLayer.geometry = geometryData;
+
+		},
+
+		// Lists the tag strings that can be associated with polygons by the PTAG chunk.
+		// TAGS { tag-string[S0] * }
+		parseTagStrings( length ) {
+
+			this.tree.tags = this.reader.getStringArray( length );
+
+		},
+
+		// Associates tags of a given type with polygons in the most recent POLS chunk.
+		// PTAG { type[ID4], ( poly[VX], tag[U2] ) * }
+		parsePolygonTagMapping( length ) {
+
+			var finalOffset = this.reader.offset + length;
+			var type = this.reader.getIDTag();
+			if ( type === 'SURF' ) this.parseMaterialIndices( finalOffset );
+			else { //PART, SMGP, COLR not supported
+
+				this.reader.skip( length - 4 );
+
+			}
+
+		},
+
+		parseMaterialIndices( finalOffset ) {
+
+			// array holds polygon index followed by material index
+			this.currentLayer.geometry.materialIndices = [];
+
+			var initialMatIndex;
+
+			while ( this.reader.offset < finalOffset ) {
+
+				var polygonIndex = this.reader.getVariableLengthIndex();
+				var materialIndex = this.reader.getUint16();
+
+				if ( ! initialMatIndex ) initialMatIndex = materialIndex; // set up first mat index
+
+				this.currentLayer.geometry.materialIndices.push( polygonIndex, materialIndex );
+
+			}
+
+		},
+
+		parseUnknownCHUNK( blockID, length ) {
+
+			console.warn( 'LWOLoader: unknown chunk type: ' + blockID + ' length: ' + length );
+
+			// print the chunk plus some bytes padding either side
+			// printBuffer( this.reader.dv.buffer, this.reader.offset - 20, length + 40 );
+
+			var data = this.reader.getString( length );
+
+			this.currentForm[ blockID ] = data;
+
+		}
+
+	};
+
+	function DataViewReader( buffer ) {
+
+		// For testing: dump whole buffer to console as a string
+		// printBuffer( buffer, 0, buffer.byteLength );
+
+		this.dv = new DataView( buffer );
+		this.offset = 0;
+
+	}
+
+	DataViewReader.prototype = {
+
+		constructor: DataViewReader,
+
+		size: function () {
+
+			return this.dv.buffer.byteLength;
+
+		},
+
+		setOffset( offset ) {
+
+			if ( offset > 0 && offset < this.dv.buffer.byteLength ) {
+
+				this.offset = offset;
+
+			} else {
+
+				console.error( 'LWOLoader: invalid buffer offset' );
+
+			}
+
+		},
+
+		endOfFile: function () {
+
+			if ( this.offset >= this.size() ) return true;
+			return false;
+
+		},
+
+		skip: function ( length ) {
+
+			this.offset += length;
+
+		},
+
+		getUint8: function () {
+
+			var value = this.dv.getUint8( this.offset );
+			this.offset += 1;
+			return value;
+
+		},
+
+		getUint16: function () {
+
+			var value = this.dv.getUint16( this.offset );
+			this.offset += 2;
+			return value;
+
+		},
+
+		getInt32: function () {
+
+			var value = this.dv.getInt32( this.offset, false );
+			this.offset += 4;
+			return value;
+
+		},
+
+		getUint32: function () {
+
+			var value = this.dv.getUint32( this.offset, false );
+			this.offset += 4;
+			return value;
+
+		},
+
+		getUint64: function () {
+
+			var low, high;
+
+			high = this.getUint32();
+			low = this.getUint32();
+			return high * 0x100000000 + low;
+
+		},
+
+		getFloat32: function () {
+
+			var value = this.dv.getFloat32( this.offset, false );
+			this.offset += 4;
+			return value;
+
+		},
+
+		getFloat32Array: function ( size ) {
+
+			var a = [];
+
+			for ( var i = 0; i < size; i ++ ) {
+
+				a.push( this.getFloat32() );
+
+			}
+
+			return a;
+
+		},
+
+		getFloat64: function () {
+
+			var value = this.dv.getFloat64( this.offset, this.littleEndian );
+			this.offset += 8;
+			return value;
+
+		},
+
+		getFloat64Array: function ( size ) {
+
+			var a = [];
+
+			for ( var i = 0; i < size; i ++ ) {
+
+				a.push( this.getFloat64() );
+
+			}
+
+			return a;
+
+		},
+
+		// get variable-length index data type
+		// VX ::= index[U2] | (index + 0xFF000000)[U4]
+		// If the index value is less than 65,280 (0xFF00),then VX === U2
+		// otherwise VX === U4 with bits 24-31 set
+		// When reading an index, if the first byte encountered is 255 (0xFF), then
+		// the four-byte form is being used and the first byte should be discarded or masked out.
+		getVariableLengthIndex() {
+
+			var firstByte = this.getUint8();
+
+			if ( firstByte === 255 ) {
+
+				return this.getUint8() * 65536 + this.getUint8() * 256 + this.getUint8();
+
+			}
+
+			return firstByte * 256 + this.getUint8();
+
+		},
+
+		// An ID tag is a sequence of 4 bytes containing 7-bit ASCII values
+		getIDTag() {
+
+			return this.getString( 4 );
+
+		},
+
+		getString: function ( size ) {
+
+			if ( size === 0 ) return;
+
+			// note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead
+			var a = [];
+
+			if ( size ) {
+
+				for ( var i = 0; i < size; i ++ ) {
+
+					a[ i ] = this.getUint8();
+
+				}
+
+			} else {
+
+				var currentChar;
+				var len = 0;
+
+				while ( currentChar !== 0 ) {
+
+					currentChar = this.getUint8();
+					if ( currentChar !== 0 ) a.push( currentChar );
+					len ++;
+
+				}
+
+				if ( ! isEven( len + 1 ) ) this.getUint8(); // if string with terminating nullbyte is uneven, extra nullbyte is added
+
+			}
+
+			return THREE.LoaderUtils.decodeText( new Uint8Array( a ) );
+
+		},
+
+		getStringArray: function ( size ) {
+
+			var a = this.getString( size );
+			a = a.split( '\0' );
+
+			return a.filter( Boolean ); // return array with any empty strings removed
+
+		}
+
+	};
+
+	// ************** UTILITY FUNCTIONS **************
+
+	function isEven( num ) {
+
+		return num % 2;
+
+	}
+
+	// calculate the length of the string in the buffer
+	// this will be string.length + nullbyte + optional padbyte to make the length even
+	function stringOffset( string ) {
+
+		return string.length + 1 + ( isEven( string.length + 1 ) ? 1 : 0 );
+
+	}
+
+	// for testing purposes, dump buffer to console
+	// printBuffer( this.reader.dv.buffer, this.reader.offset, length );
+	function printBuffer( buffer, from, to ) {
+
+		console.log( THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ) );
+
+	}
+
+	return LWOLoader;
+
+} )();

+ 883 - 27
examples/js/loaders/SVGLoader.js

@@ -56,37 +56,37 @@ THREE.SVGLoader.prototype = {
 
 				case 'path':
 					style = parseStyle( node, style );
-					if ( node.hasAttribute( 'd' ) && isVisible( style ) ) path = parsePathNode( node, style );
+					if ( node.hasAttribute( 'd' ) ) path = parsePathNode( node, style );
 					break;
 
 				case 'rect':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parseRectNode( node, style );
+					path = parseRectNode( node, style );
 					break;
 
 				case 'polygon':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parsePolygonNode( node, style );
+					path = parsePolygonNode( node, style );
 					break;
 
 				case 'polyline':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parsePolylineNode( node, style );
+					path = parsePolylineNode( node, style );
 					break;
 
 				case 'circle':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parseCircleNode( node, style );
+					path = parseCircleNode( node, style );
 					break;
 
 				case 'ellipse':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parseEllipseNode( node, style );
+					path = parseEllipseNode( node, style );
 					break;
 
 				case 'line':
 					style = parseStyle( node, style );
-					if ( isVisible( style ) ) path = parseLineNode( node, style );
+					path = parseLineNode( node, style );
 					break;
 
 				default:
@@ -96,10 +96,18 @@ THREE.SVGLoader.prototype = {
 
 			if ( path ) {
 
+				if ( style.fill !== undefined && style.fill !== 'none' ) {
+
+					path.color.setStyle( style.fill );
+
+				}
+
 				transformPath( path, currentTransform );
 
 				paths.push( path );
 
+				path.userData = { node: node, style: style };
+
 			}
 
 			var nodes = node.childNodes;
@@ -121,7 +129,6 @@ THREE.SVGLoader.prototype = {
 		function parsePathNode( node, style ) {
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 
 			var point = new THREE.Vector2();
 			var control = new THREE.Vector2();
@@ -544,7 +551,6 @@ THREE.SVGLoader.prototype = {
 			var h = parseFloat( node.getAttribute( 'height' ) );
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 			path.moveTo( x + 2 * rx, y );
 			path.lineTo( x + w - 2 * rx, y );
 			if ( rx !== 0 || ry !== 0 ) path.bezierCurveTo( x + w, y, x + w, y, x + w, y + 2 * ry );
@@ -590,7 +596,6 @@ THREE.SVGLoader.prototype = {
 			var regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g;
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 
 			var index = 0;
 
@@ -622,7 +627,6 @@ THREE.SVGLoader.prototype = {
 			var regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g;
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 
 			var index = 0;
 
@@ -644,7 +648,6 @@ THREE.SVGLoader.prototype = {
 			subpath.absarc( x, y, r, 0, Math.PI * 2 );
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 			path.subPaths.push( subpath );
 
 			return path;
@@ -662,7 +665,6 @@ THREE.SVGLoader.prototype = {
 			subpath.absellipse( x, y, rx, ry, 0, Math.PI * 2 );
 
 			var path = new THREE.ShapePath();
-			path.color.setStyle( style.fill );
 			path.subPaths.push( subpath );
 
 			return path;
@@ -691,16 +693,37 @@ THREE.SVGLoader.prototype = {
 
 			style = Object.assign( {}, style ); // clone style
 
-			if ( node.hasAttribute( 'fill' ) ) style.fill = node.getAttribute( 'fill' );
-			if ( node.style.fill !== '' ) style.fill = node.style.fill;
+			function addStyle( svgName, jsName, adjustFunction ) {
 
-			return style;
+				if ( adjustFunction === undefined ) adjustFunction = function copy( v ) { return v; };
 
-		}
+				if ( node.hasAttribute( svgName ) ) style[ jsName ] = adjustFunction( node.getAttribute( svgName ) );
+				if ( node.style[ svgName ] !== '' ) style[ jsName ] = adjustFunction( node.style[ svgName ] );
+
+			}
+
+			function clamp( v ) {
+
+				return Math.max( 0, Math.min( 1, v ) );
+
+			}
+
+			function positive( v ) {
+
+				return Math.max( 0, v );
 
-		function isVisible( style ) {
+			}
+
+			addStyle( 'fill', 'fill' );
+			addStyle( 'fill-opacity', 'fillOpacity', clamp );
+			addStyle( 'stroke', 'stroke' );
+			addStyle( 'stroke-opacity', 'strokeOpacity', clamp );
+			addStyle( 'stroke-width', 'strokeWidth', positive );
+			addStyle( 'stroke-linejoin', 'strokeLineJoin' );
+			addStyle( 'stroke-linecap', 'strokeLineCap' );
+			addStyle( 'stroke-miterlimit', 'strokeMiterLimit', positive );
 
-			return style.fill !== 'none' && style.fill !== 'transparent';
+			return style;
 
 		}
 
@@ -750,7 +773,7 @@ THREE.SVGLoader.prototype = {
 				return null;
 			}
 
-			var transform = parseTransformNode( node );
+			var transform = parseNodeTransform( node );
 
 			if ( transform ) {
 
@@ -767,13 +790,13 @@ THREE.SVGLoader.prototype = {
 
 		}
 
-		function parseTransformNode( node ) {
+		function parseNodeTransform( node ) {
 
 			var transform = new THREE.Matrix3();
 			var currentTransform = tempTransform0;
 			var transformsTexts = node.getAttribute( 'transform' ).split( ' ' );
 
-			for ( var tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex-- ) {
+			for ( var tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex -- ) {
 
 				var transformText = transformsTexts[ tIndex ];
 				var openParPos = transformText.indexOf( "(" );
@@ -920,9 +943,6 @@ THREE.SVGLoader.prototype = {
 
 			var isRotated = isTransformRotated( m );
 
-			var tempV2 = new THREE.Vector2();
-			var tempV3 = new THREE.Vector3();
-
 			var subPaths = path.subPaths;
 
 			for ( var i = 0, n = subPaths.length; i < n; i++ ) {
@@ -1000,9 +1020,13 @@ THREE.SVGLoader.prototype = {
 		var tempTransform1 = new THREE.Matrix3();
 		var tempTransform2 = new THREE.Matrix3();
 		var tempTransform3 = new THREE.Matrix3();
+		var tempV2 = new THREE.Vector2();
+		var tempV3 = new THREE.Vector3();
 
 		var currentTransform = new THREE.Matrix3();
 
+		var scope = this;
+
 		console.time( 'THREE.SVGLoader: DOMParser' );
 
 		var xml = new DOMParser().parseFromString( text, 'image/svg+xml' ); // application/xml
@@ -1011,15 +1035,847 @@ THREE.SVGLoader.prototype = {
 
 		console.time( 'THREE.SVGLoader: Parse' );
 
-		parseNode( xml.documentElement, { fill: '#000' } );
+		parseNode( xml.documentElement, {
+			fill: '#000',
+			fillOpacity: 1,
+			strokeOpacity: 1,
+			strokeWidth: 1,
+			strokeLineJoin: 'miter',
+			strokeLineCap: 'butt',
+			strokeMiterLimit: 4
+		} );
+
+		var data = { paths: paths, xml: xml.documentElement };
 
 		// console.log( paths );
 
 
 		console.timeEnd( 'THREE.SVGLoader: Parse' );
 
-		return paths;
+		return data;
+
+	}
+
+};
+
+THREE.SVGLoader.getStrokeStyle = function ( width, color, opacity, lineJoin, lineCap,  miterLimit ) {
+
+	// Param width: Stroke width
+	// Param color: As returned by THREE.Color.getStyle()
+	// Param opacity: 0 (transparent) to 1 (opaque)
+	// Param lineJoin: One of "round", "bevel", "miter" or "miter-limit"
+	// Param lineCap: One of "round", "square" or "butt"
+	// Param miterLimit: Maximum join length, in multiples of the "width" parameter (join is truncated if it exceeds that distance)
+	// Returns style object
+
+	width = width !== undefined ? width : 1;
+	color = color !== undefined ? color : '#000';
+	opacity = opacity !== undefined ? opacity : 1;
+	lineJoin = lineJoin !== undefined ? lineJoin : 'miter';
+	lineCap = lineCap !== undefined ? lineCap : 'butt';
+	miterLimit = miterLimit !== undefined ? miterLimit : 4;
+
+	return {
+		strokeColor: color,
+		strokeWidth: width,
+		strokeLineJoin: lineJoin,
+		strokeLineCap: lineCap,
+		strokeMiterLimit: miterLimit
+	};
+
+};
+
+THREE.SVGLoader.pointsToStroke = function ( points, style, arcDivisions, minDistance ) {
+
+	// Generates a stroke with some witdh around the given path.
+	// The path can be open or closed (last point equals to first point)
+	// Param points: Array of Vector2D (the path). Minimum 2 points.
+	// Param style: Object with SVG properties as returned by SVGLoader.getStrokeStyle(), or SVGLoader.parse() in the path.userData.style object
+	// Params arcDivisions: Arc divisions for round joins and endcaps. (Optional)
+	// Param minDistance: Points closer to this distance will be merged. (Optional)
+	// Returns BufferGeometry with stroke triangles (In plane z = 0). UV coordinates are generated ('u' along path. 'v' across it, from left to right)
+
+	var vertices = [];
+	var normals = [];
+	var uvs = [];
+
+	if ( THREE.SVGLoader.pointsToStrokeWithBuffers( points, style, arcDivisions, minDistance, vertices, normals, uvs ) === 0 ) {
+
+		return null;
 
 	}
 
+	var geometry = new THREE.BufferGeometry();
+	geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
+	geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );
+	geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) );
+
+	return geometry;
+
 };
+
+THREE.SVGLoader.pointsToStrokeWithBuffers = function () {
+
+	var tempV2_1 = new THREE.Vector2();
+	var tempV2_2 = new THREE.Vector2();
+	var tempV2_3 = new THREE.Vector2();
+	var tempV2_4 = new THREE.Vector2();
+	var tempV2_5 = new THREE.Vector2();
+	var tempV2_6 = new THREE.Vector2();
+	var tempV2_7 = new THREE.Vector2();
+	var tempV3_1 = new THREE.Vector3();
+	var lastPointL = new THREE.Vector2();
+	var lastPointR = new THREE.Vector2();
+	var point0L = new THREE.Vector2();
+	var point0R = new THREE.Vector2();
+	var currentPointL = new THREE.Vector2();
+	var currentPointR = new THREE.Vector2();
+	var nextPointL = new THREE.Vector2();
+	var nextPointR = new THREE.Vector2();
+	var innerPoint = new THREE.Vector2();
+	var outerPoint = new THREE.Vector2();
+	var tempTransform0 = new THREE.Matrix3();
+	var tempTransform1 = new THREE.Matrix3();
+	var tempTransform2 = new THREE.Matrix3();
+
+	return function ( points, style, arcDivisions, minDistance, vertices, normals, uvs, vertexOffset ) {
+
+		// This function can be called to update existing arrays or buffers.
+		// Accepts same parameters as pointsToStroke, plus the buffers and optional offset.
+		// Param vertexOffset: Offset vertices to start writing in the buffers (3 elements/vertex for vertices and normals, and 2 elements/vertex for uvs)
+		// Returns number of written vertices / normals / uvs pairs
+		// if 'vertices' parameter is undefined no triangles will be generated, but the returned vertices count will still be valid (useful to preallocate the buffers)
+		// 'normals' and 'uvs' buffers are optional
+
+		arcLengthDivisions = arcDivisions !== undefined ? arcDivisions : 12;
+		minDistance = minDistance !== undefined ? minDistance : 0.001;
+		vertexOffset = vertexOffset !== undefined ? vertexOffset : 0;
+
+		// First ensure there are no duplicated points
+		points = removeDuplicatedPoints( points );
+
+		var numPoints = points.length;
+
+		if ( numPoints < 2 ) return 0;
+
+		var isClosed = points[ 0 ].equals( points[ numPoints - 1 ] );
+
+		var currentPoint;
+		var previousPoint = points[ 0 ];
+		var nextPoint;
+
+		var strokeWidth2 = style.strokeWidth / 2;
+
+		var deltaU = 1 / ( numPoints - 1 );
+		var u0 = 0;
+
+		var innerSideModified;
+		var joinIsOnLeftSide;
+		var isMiter;
+		var initialJoinIsOnLeftSide = false;
+
+		var numVertices = 0;
+		var currentCoordinate = vertexOffset * 3;
+		var currentCoordinateUV = vertexOffset * 2;
+
+		// Get initial left and right stroke points
+		getNormal( points[ 0 ], points[ 1 ], tempV2_1 ).multiplyScalar( strokeWidth2 );
+		lastPointL.copy( points[ 0 ] ).sub( tempV2_1 );
+		lastPointR.copy( points[ 0 ] ).add( tempV2_1 );
+		point0L.copy( lastPointL );
+		point0R.copy( lastPointR );
+
+		for ( var iPoint = 1; iPoint < numPoints; iPoint ++ ) {
+
+			currentPoint = points[ iPoint ];
+
+			// Get next point
+			if ( iPoint === numPoints - 1 ) {
+
+				if ( isClosed ) {
+
+					// Skip duplicated initial point
+					nextPoint = points[ 1 ];
+
+				}
+				else nextPoint = undefined;
+
+			}
+			else {
+
+				nextPoint = points[ iPoint + 1 ];
+
+			}
+
+			// Normal of previous segment in tempV2_1
+			var normal1 = tempV2_1;
+			getNormal( previousPoint, currentPoint, normal1 );
+
+			tempV2_3.copy( normal1 ).multiplyScalar( strokeWidth2 );
+			currentPointL.copy( currentPoint ).sub( tempV2_3 );
+			currentPointR.copy( currentPoint ).add( tempV2_3 );
+
+			var u1 = u0 + deltaU;
+
+			innerSideModified = false;
+
+			if ( nextPoint !== undefined ) {
+
+				// Normal of next segment in tempV2_2
+				getNormal( currentPoint, nextPoint, tempV2_2 );
+
+				tempV2_3.copy( tempV2_2 ).multiplyScalar( strokeWidth2 );
+				nextPointL.copy( currentPoint ).sub( tempV2_3 );
+				nextPointR.copy( currentPoint ).add( tempV2_3 );
+
+				joinIsOnLeftSide = true;
+				tempV2_3.subVectors( nextPoint, previousPoint );
+				if ( normal1.dot( tempV2_3 ) < 0 ) {
+
+					joinIsOnLeftSide = false;
+
+				}
+				if ( iPoint === 1 ) initialJoinIsOnLeftSide = joinIsOnLeftSide;
+
+				tempV2_3.subVectors( nextPoint, currentPoint )
+				var maxInnerDistance = tempV2_3.normalize();
+				var dot = Math.abs( normal1.dot( tempV2_3 ) );
+
+				// If path is straight, don't create join
+				if ( dot !== 0 ) {
+
+					// Compute inner and outer segment intersections
+					var miterSide = strokeWidth2 / dot;
+					tempV2_3.multiplyScalar( - miterSide );
+					tempV2_4.subVectors( currentPoint, previousPoint );
+					tempV2_5.copy( tempV2_4 ).setLength( miterSide ).add( tempV2_3 );
+					innerPoint.copy( tempV2_5 ).negate();
+					var miterLength2 = tempV2_5.length();
+					var segmentLengthPrev = tempV2_4.length();
+					tempV2_4.divideScalar( segmentLengthPrev );
+					tempV2_6.subVectors( nextPoint, currentPoint );
+					var segmentLengthNext = tempV2_6.length();
+					tempV2_6.divideScalar( segmentLengthNext );
+					// Check that previous and next segments doesn't overlap with the innerPoint of intersection
+					if ( tempV2_4.dot( innerPoint ) < segmentLengthPrev && tempV2_6.dot( innerPoint ) < segmentLengthNext ) {
+
+						innerSideModified = true;
+
+					}
+					outerPoint.copy( tempV2_5 ).add( currentPoint );
+					innerPoint.add( currentPoint );
+
+					isMiter = false;
+
+					if ( innerSideModified ) {
+
+						if ( joinIsOnLeftSide ) {
+
+							nextPointR.copy( innerPoint );
+							currentPointR.copy( innerPoint );
+
+						}
+						else {
+
+							nextPointL.copy( innerPoint );
+							currentPointL.copy( innerPoint );
+
+						}
+
+					}
+					else {
+
+						// The segment triangles are generated here if there was overlapping
+
+						makeSegmentTriangles();
+
+					}
+
+					switch ( style.strokeLineJoin ) {
+
+						case 'bevel':
+
+							makeSegmentWithBevelJoin( joinIsOnLeftSide, innerSideModified, u1 );
+
+							break;
+
+						case 'round':
+
+							// Segment triangles
+
+							createSegmentTrianglesWithMiddleSection( joinIsOnLeftSide, innerSideModified );
+
+							// Join triangles
+
+							if ( joinIsOnLeftSide ) {
+
+								makeCircularSector( currentPoint, currentPointL, nextPointL, u1, 0 );
+
+							}
+							else {
+
+								makeCircularSector( currentPoint, nextPointR, currentPointR, u1, 1 );
+
+							}
+
+							break;
+
+						case 'miter':
+						case 'miter-clip':
+						default:
+
+							var miterFraction = ( strokeWidth2 * style.strokeMiterLimit ) / miterLength2;
+
+							if ( miterFraction < 1 ) {
+
+								// The join miter length exceeds the miter limit
+
+								if ( style.strokeLineJoin !== 'miter-clip' ) {
+
+									makeSegmentWithBevelJoin( joinIsOnLeftSide, innerSideModified, u1 );
+									break;
+
+								}
+								else {
+
+									// Segment triangles
+
+									createSegmentTrianglesWithMiddleSection( joinIsOnLeftSide, innerSideModified );
+
+									// Miter-clip join triangles
+
+									if ( joinIsOnLeftSide ) {
+
+										tempV2_6.subVectors( outerPoint, currentPointL ).multiplyScalar( miterFraction ).add( currentPointL );
+										tempV2_7.subVectors( outerPoint, nextPointL ).multiplyScalar( miterFraction ).add( nextPointL );
+
+										addVertex( currentPointL, u1, 0 );
+										addVertex( tempV2_6, u1, 0 );
+										addVertex( currentPoint, u1, 0.5 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( tempV2_6, u1, 0 );
+										addVertex( tempV2_7, u1, 0 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( tempV2_7, u1, 0 );
+										addVertex( nextPointL, u1, 0 );
+
+									}
+									else {
+
+										tempV2_6.subVectors( outerPoint, currentPointR ).multiplyScalar( miterFraction ).add( currentPointR );
+										tempV2_7.subVectors( outerPoint, nextPointR ).multiplyScalar( miterFraction ).add( nextPointR );
+
+										addVertex( currentPointR, u1, 1 );
+										addVertex( tempV2_6, u1, 1 );
+										addVertex( currentPoint, u1, 0.5 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( tempV2_6, u1, 1 );
+										addVertex( tempV2_7, u1, 1 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( tempV2_7, u1, 1 );
+										addVertex( nextPointR, u1, 1 );
+
+									}
+
+								}
+
+							}
+							else {
+
+								// Miter join segment triangles
+
+								if ( innerSideModified ) {
+
+									// Optimized segment + join triangles
+
+									if ( joinIsOnLeftSide ) {
+
+										addVertex( lastPointR, u0, 1 );
+										addVertex( lastPointL, u0, 0 );
+										addVertex( outerPoint, u1, 0 );
+
+										addVertex( lastPointR, u0, 1 );
+										addVertex( outerPoint, u1, 0 );
+										addVertex( innerPoint, u1, 1 );
+
+									}
+									else {
+
+										addVertex( lastPointR, u0, 1 );
+										addVertex( lastPointL, u0, 0 );
+										addVertex( outerPoint, u1, 1 );
+
+										addVertex( lastPointL, u0, 0 );
+										addVertex( innerPoint, u1, 0 );
+										addVertex( outerPoint, u1, 1 );
+
+									}
+
+
+									if ( joinIsOnLeftSide ) {
+
+										nextPointL.copy( outerPoint );
+
+									}
+									else {
+
+										nextPointR.copy( outerPoint );
+
+									}
+
+
+								}
+								else {
+
+									// Add extra miter join triangles
+
+									if ( joinIsOnLeftSide ) {
+
+										addVertex( currentPointL, u1, 0 );
+										addVertex( outerPoint, u1, 0 );
+										addVertex( currentPoint, u1, 0.5 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( outerPoint, u1, 0 );
+										addVertex( nextPointL, u1, 0 );
+
+									}
+									else {
+
+										addVertex( currentPointR, u1, 1 );
+										addVertex( outerPoint, u1, 1 );
+										addVertex( currentPoint, u1, 0.5 );
+
+										addVertex( currentPoint, u1, 0.5 );
+										addVertex( outerPoint, u1, 1 );
+										addVertex( nextPointR, u1, 1 );
+
+									}
+
+								}
+
+								isMiter = true;
+
+							}
+
+							break;
+
+					}
+
+				}
+				else {
+
+					// The segment triangles are generated here when two consecutive points are collinear
+
+					makeSegmentTriangles();
+
+				}
+
+			}
+			else {
+
+				// The segment triangles are generated here if it is the ending segment
+
+				makeSegmentTriangles();
+
+			}
+
+			if ( ! isClosed && iPoint === numPoints - 1 ) {
+
+				// Start line endcap
+				addCapGeometry( points[ 0 ], point0L, point0R, joinIsOnLeftSide, true, u0 );
+
+			}
+
+			// Increment loop variables
+
+			u0 = u1;
+
+			previousPoint = currentPoint;
+
+			lastPointL.copy( nextPointL );
+			lastPointR.copy( nextPointR );
+
+		}
+
+		if ( ! isClosed ) {
+
+			// Ending line endcap
+			addCapGeometry( currentPoint, currentPointL, currentPointR, joinIsOnLeftSide, false, u1 );
+
+		}
+		else if ( innerSideModified && vertices ) {
+
+			// Modify path first segment vertices to adjust to the segments inner and outer intersections
+
+			var lastOuter = outerPoint;
+			var lastInner = innerPoint;
+			if ( initialJoinIsOnLeftSide !== joinIsOnLeftSide) {
+				lastOuter = innerPoint;
+				lastInner = outerPoint;
+			}
+
+			if ( joinIsOnLeftSide ) {
+
+				lastInner.toArray( vertices, 0 * 3 );
+				lastInner.toArray( vertices, 3 * 3 );
+
+				if ( isMiter ) {
+
+					lastOuter.toArray( vertices, 1 * 3 );
+				}
+
+			}
+			else {
+
+				lastInner.toArray( vertices, 1 * 3 );
+				lastInner.toArray( vertices, 3 * 3 );
+
+				if ( isMiter ) {
+
+					lastOuter.toArray( vertices, 0 * 3 );
+				}
+
+			}
+
+		}
+
+		return numVertices;
+
+		// -- End of algorithm
+
+		// -- Functions
+
+		function getNormal( p1, p2, result ) {
+
+			result.subVectors( p2, p1 );
+			return result.set( - result.y, result.x ).normalize();
+
+		}
+
+		function addVertex( position, u, v ) {
+
+			if ( vertices ) {
+
+				vertices[ currentCoordinate ] = position.x;
+				vertices[ currentCoordinate + 1 ] = position.y;
+				vertices[ currentCoordinate + 2 ] = 0;
+
+				if ( normals ) {
+
+					normals[ currentCoordinate ] = 0;
+					normals[ currentCoordinate + 1 ] = 0;
+					normals[ currentCoordinate + 2 ] = 1;
+
+				}
+
+				currentCoordinate += 3;
+
+				if ( uvs ) {
+
+					uvs[ currentCoordinateUV ] = u;
+					uvs[ currentCoordinateUV + 1 ] = v;
+
+					currentCoordinateUV += 2;
+
+				}
+
+			}
+
+			numVertices += 3;
+
+		}
+
+		function makeCircularSector( center, p1, p2, u, v ) {
+
+			// param p1, p2: Points in the circle arc.
+			// p1 and p2 are in clockwise direction.
+
+			tempV2_1.copy( p1 ).sub( center ).normalize();
+			tempV2_2.copy( p2 ).sub( center ).normalize();
+
+			var angle = Math.PI;
+			var dot = tempV2_1.dot( tempV2_2 );
+			if ( Math.abs( dot ) < 1 ) angle = Math.abs( Math.acos( dot ) );
+
+			angle /= arcLengthDivisions;
+
+			tempV2_3.copy( p1 );
+
+			for ( var i = 0, il = arcLengthDivisions - 1; i < il; i++ ) {
+
+				tempV2_4.copy( tempV2_3 ).rotateAround( center, angle );
+
+				addVertex( tempV2_3, u, v );
+				addVertex( tempV2_4, u, v );
+				addVertex( center, u, 0.5 );
+
+				tempV2_3.copy( tempV2_4 );
+			}
+
+			addVertex( tempV2_4, u, v );
+			addVertex( p2, u, v );
+			addVertex( center, u, 0.5 );
+
+		}
+
+		function makeSegmentTriangles() {
+
+			addVertex( lastPointR, u0, 1 );
+			addVertex( lastPointL, u0, 0 );
+			addVertex( currentPointL, u1, 0 );
+
+			addVertex( lastPointR, u0, 1 );
+			addVertex( currentPointL, u1, 1 );
+			addVertex( currentPointR, u1, 0 );
+
+		}
+
+		function makeSegmentWithBevelJoin( joinIsOnLeftSide, innerSideModified, u ) {
+
+			if ( innerSideModified ) {
+
+				// Optimized segment + bevel triangles
+
+				if ( joinIsOnLeftSide ) {
+
+					// Path segments triangles
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( lastPointL, u0, 0 );
+					addVertex( currentPointL, u1, 0 );
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( currentPointL, u1, 0 );
+					addVertex( innerPoint, u1, 1 );
+
+					// Bevel join triangle
+
+					addVertex( currentPointL, u, 0 );
+					addVertex( nextPointL, u, 0 );
+					addVertex( innerPoint, u, 0.5 );
+
+				}
+				else {
+
+					// Path segments triangles
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( lastPointL, u0, 0 );
+					addVertex( currentPointR, u1, 1 );
+
+					addVertex( lastPointL, u0, 0 );
+					addVertex( innerPoint, u1, 0 );
+					addVertex( currentPointR, u1, 1 );
+
+					// Bevel join triangle
+
+					addVertex( currentPointR, u, 1 );
+					addVertex( nextPointR, u, 0 );
+					addVertex( innerPoint, u, 0.5 );
+
+				}
+
+			}
+			else {
+
+				// Bevel join triangle. The segment triangles are done in the main loop
+
+				if ( joinIsOnLeftSide ) {
+
+					addVertex( currentPointL, u, 0 );
+					addVertex( nextPointL, u, 0 );
+					addVertex( currentPoint, u, 0.5 );
+
+				}
+				else {
+
+					addVertex( currentPointR, u, 1 );
+					addVertex( nextPointR, u, 0 );
+					addVertex( currentPoint, u, 0.5 );
+
+				}
+
+			}
+
+		}
+
+		function createSegmentTrianglesWithMiddleSection( joinIsOnLeftSide, innerSideModified ) {
+
+			if ( innerSideModified ) {
+
+				if ( joinIsOnLeftSide ) {
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( lastPointL, u0, 0 );
+					addVertex( currentPointL, u1, 0 );
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( currentPointL, u1, 0 );
+					addVertex( innerPoint, u1, 1 );
+
+					addVertex( currentPointL, u0, 0 );
+					addVertex( currentPoint, u1, 0.5 );
+					addVertex( innerPoint, u1, 1 );
+
+					addVertex( currentPoint, u1, 0.5 );
+					addVertex( nextPointL, u0, 0 );
+					addVertex( innerPoint, u1, 1 );
+
+				}
+				else {
+
+					addVertex( lastPointR, u0, 1 );
+					addVertex( lastPointL, u0, 0 );
+					addVertex( currentPointR, u1, 1 );
+
+					addVertex( lastPointL, u0, 0 );
+					addVertex( innerPoint, u1, 0 );
+					addVertex( currentPointR, u1, 1 );
+
+					addVertex( currentPointR, u0, 1 );
+					addVertex( innerPoint, u1, 0 );
+					addVertex( currentPoint, u1, 0.5 );
+
+					addVertex( currentPoint, u1, 0.5 );
+					addVertex( innerPoint, u1, 0 );
+					addVertex( nextPointR, u0, 1 );
+
+				}
+
+			}
+
+		}
+
+		function addCapGeometry( center, p1, p2, joinIsOnLeftSide, start, u ) {
+
+			// param center: End point of the path
+			// param p1, p2: Left and right cap points
+
+			switch ( style.strokeLineCap ) {
+
+				case 'round':
+
+					if ( start ) {
+
+						makeCircularSector( center, p2, p1, u, 0.5 );
+
+					}
+					else {
+
+						makeCircularSector( center, p1, p2, u, 0.5 );
+
+					}
+
+					break;
+
+				case 'square':
+
+					if ( start ) {
+
+						tempV2_1.subVectors( p1, center );
+						tempV2_2.set( tempV2_1.y, - tempV2_1.x );
+
+						tempV2_3.addVectors( tempV2_1, tempV2_2 ).add( center );
+						tempV2_4.subVectors( tempV2_2, tempV2_1 ).add( center );
+
+						// Modify already existing vertices
+						if ( joinIsOnLeftSide ) {
+
+							tempV2_3.toArray( vertices, 1 * 3 );
+							tempV2_4.toArray( vertices, 0 * 3 );
+							tempV2_4.toArray( vertices, 3 * 3 );
+
+						}
+						else {
+
+							tempV2_3.toArray( vertices, 1 * 3 );
+							tempV2_3.toArray( vertices, 3 * 3 );
+							tempV2_4.toArray( vertices, 0 * 3 );
+
+						}
+
+					}
+					else {
+
+						tempV2_1.subVectors( p2, center );
+						tempV2_2.set( tempV2_1.y, - tempV2_1.x );
+
+						tempV2_3.addVectors( tempV2_1, tempV2_2 ).add( center );
+						tempV2_4.subVectors( tempV2_2, tempV2_1 ).add( center );
+
+						var vl = vertices.length;
+
+						// Modify already existing vertices
+						if ( joinIsOnLeftSide ) {
+
+							tempV2_3.toArray( vertices, vl - 1 * 3 );
+							tempV2_4.toArray( vertices, vl - 2 * 3 );
+							tempV2_4.toArray( vertices, vl - 4 * 3 );
+
+						}
+						else {
+
+							tempV2_3.toArray( vertices, vl - 2 * 3 );
+							tempV2_4.toArray( vertices, vl - 1 * 3 );
+							tempV2_4.toArray( vertices, vl - 4 * 3 );
+
+						}
+
+					}
+
+					break;
+
+				case 'butt':
+				default:
+
+					// Nothing to do here
+					break;
+
+			}
+
+		}
+
+		function removeDuplicatedPoints( points ) {
+
+			// Creates a new array if necessary with duplicated points removed.
+			// This does not remove duplicated initial and ending points of a closed path.
+
+			var dupPoints = false;
+			for ( var i = 1, n = points.length - 1; i < n; i ++ ) {
+
+				if ( points[ i ].distanceTo( points[ i + 1 ] ) < minDistance ) {
+
+					dupPoints = true;
+					break;
+
+				}
+
+			}
+
+			if ( ! dupPoints ) return points;
+
+			var newPoints = [];
+			newPoints.push( points[ 0 ] );
+
+			for ( var i = 1, n = points.length - 1; i < n; i ++ ) {
+
+				if ( points[ i ].distanceTo( points[ i + 1 ] ) >= minDistance ) {
+
+					newPoints.push( points[ i ] );
+
+				}
+			}
+
+			newPoints.push( points[ points.length - 1 ] );
+
+			return newPoints;
+
+		}
+	};
+
+}();

+ 1 - 4
examples/js/loaders/ctm/CTMLoader.js

@@ -10,11 +10,8 @@
 
 THREE.CTMLoader = function () {
 
-	THREE.Loader.call( this );
-
 };
 
-THREE.CTMLoader.prototype = Object.create( THREE.Loader.prototype );
 THREE.CTMLoader.prototype.constructor = THREE.CTMLoader;
 
 // Load multiple CTM parts defined in JSON
@@ -58,7 +55,7 @@ THREE.CTMLoader.prototype.loadParts = function ( url, callback, parameters ) {
 
 				for ( var i = 0; i < jsonObject.materials.length; i ++ ) {
 
-					materials[ i ] = scope.createMaterial( jsonObject.materials[ i ], basePath );
+					materials[ i ] = THREE.Loader.prototype.createMaterial( jsonObject.materials[ i ], basePath );
 
 				}
 

+ 1 - 2
examples/js/loaders/sea3d/SEA3D.js

@@ -3240,9 +3240,8 @@ SEA3D.File.prototype.load = function ( url ) {
 
 	xhr.open( "GET", url, true );
 
-	if (!this.config.path) {
+	if ( ! this.config.path ) {
 
-//		this.config.path = THREE.Loader.prototype.extractUrlBase( url );
 		this.config.path = THREE.LoaderUtils.extractUrlBase( url );
 
 	}

+ 1 - 1
examples/js/nodes/core/StructNode.js

@@ -49,7 +49,7 @@ StructNode.prototype.generate = function ( builder, output ) {
 
 	} else {
 
-		return builder.format( '( ' + src + ' )', this.getType( builder ), output );
+		return builder.format( '( ' + this.src + ' )', this.getType( builder ), output );
 
 	}
 

+ 26 - 0
examples/js/nodes/materials/NodeMaterial.js

@@ -11,11 +11,21 @@ function NodeMaterial( vertex, fragment ) {
 
 	THREE.ShaderMaterial.call( this );
 
+	var self = this;
+
 	this.vertex = vertex || new RawNode( new PositionNode( PositionNode.PROJECTION ) );
 	this.fragment = fragment || new RawNode( new ColorNode( 0xFF0000 ) );
 
 	this.updaters = [];
 
+	// it fix the programCache and share the code with others materials
+
+	this.onBeforeCompile.toString = function() {
+
+		return self.needsCompile;
+
+	};
+
 }
 
 NodeMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype );
@@ -34,6 +44,22 @@ Object.defineProperties( NodeMaterial.prototype, {
 
 		}
 
+	},
+
+	needsUpdate: {
+
+		set: function ( value ) {
+
+			this.needsCompile = value;
+
+		},
+
+		get: function () {
+
+			return this.needsCompile;
+
+		}
+
 	}
 
 } );

+ 1 - 1
examples/js/objects/Fire.js

@@ -431,7 +431,7 @@ THREE.Fire = function ( geometry, options ) {
 
 	};
 
-	this.onBeforeRender = function ( renderer, scene, camera ) {
+	this.onBeforeRender = function ( renderer ) {
 
 		var delta = this.clock.getDelta();
 		if ( delta > 0.1 ) {

+ 32 - 34
examples/js/objects/LightningStorm.js

@@ -3,53 +3,53 @@
  *
  * @fileoverview Lightning strike object generator
  *
- * 
+ *
  * Usage
- * 
+ *
  * var myStorm = new THREE.LightningStorm( paramsObject );
  * myStorm.position.set( ... );
  * scene.add( myStorm );
  * ...
  * myStorm.update( currentTime );
- * 
+ *
  * The "currentTime" can only go forwards or be stopped.
- * 
- * 
+ *
+ *
  * LightningStorm parameters:
  *
  * @param {double} size Size of the storm. If no 'onRayPosition' parameter is defined, it means the side of the rectangle the storm covers.
  *
  * @param {double} minHeight Minimum height a ray can start at. If no 'onRayPosition' parameter is defined, it means the height above plane y = 0.
- * 
+ *
  * @param {double} maxHeight Maximum height a ray can start at. If no 'onRayPosition' parameter is defined, it means the height above plane y = 0.
- * 
+ *
  * @param {double} maxSlope The maximum inclination slope of a ray. If no 'onRayPosition' parameter is defined, it means the slope relative to plane y = 0.
- * 
+ *
  * @param {integer} maxLightnings Greater than 0. The maximum number of simultaneous rays.
- * 
+ *
  * @param {double} lightningMinPeriod minimum time between two consecutive rays.
- * 
+ *
  * @param {double} lightningMaxPeriod maximum time between two consecutive rays.
- * 
+ *
  * @param {double} lightningMinDuration The minimum time a ray can last.
- * 
+ *
  * @param {double} lightningMaxDuration The maximum time a ray can last.
- * 
+ *
  * @param {Object} lightningParameters The parameters for created rays. See THREE.LightningStrike (geometry)
- * 
+ *
  * @param {Material} lightningMaterial The THREE.Material used for the created rays.
- * 
+ *
  * @param {function} onRayPosition Optional callback with two Vector3 parameters (source, dest). You can set here the start and end points for each created ray, using the standard size, minHeight, etc parameters and other values in your algorithm.
- * 
+ *
  * @param {function} onLightningDown This optional callback is called with one parameter (lightningStrike) when a ray ends propagating, so it has hit the ground.
- * 
+ *
  *
 */
 
 THREE.LightningStorm = function ( stormParams ) {
 
 	THREE.Object3D.call( this );
-	
+
 	// Parameters
 
 	stormParams = stormParams || {};
@@ -71,21 +71,20 @@ THREE.LightningStorm = function ( stormParams ) {
 	this.lightningParameters = THREE.LightningStrike.copyParameters( stormParams.lightningParameters, stormParams.lightningParameters );
 
 	this.lightningParameters.isEternal = false;
-	
+
 	this.lightningMaterial = stormParams.lightningMaterial !== undefined ? stormParams.lightningMaterial : new THREE.MeshBasicMaterial( { color: 0xB0FFFF } );
 
 	if ( stormParams.onRayPosition !== undefined ) {
 
 		this.onRayPosition = stormParams.onRayPosition;
 
-	}
-	else {
+	} else {
 
-		this.onRayPosition = function( source, dest ) {
+		this.onRayPosition = function ( source, dest ) {
 
 			dest.set( ( Math.random() - 0.5 ) * stormParams.size, 0, ( Math.random() - 0.5 ) * stormParams.size );
-			
-			var height = THREE.Math.lerp( stormParams.minHeight, stormParams.maxHeight, Math.random() );;
+
+			var height = THREE.Math.lerp( stormParams.minHeight, stormParams.maxHeight, Math.random() );
 
 			source.set( stormParams.maxSlope * ( 2 * Math.random() - 1 ), 1, stormParams.maxSlope * ( 2 * Math.random() - 1 ) ).multiplyScalar( height ).add( dest );
 
@@ -102,7 +101,7 @@ THREE.LightningStorm = function ( stormParams ) {
 	this.lightningsMeshes = [];
 	this.deadLightningsMeshes = [];
 
-	for ( var i = 0; i < this.stormParams.maxLightnings; i++ ) {
+	for ( var i = 0; i < this.stormParams.maxLightnings; i ++ ) {
 
 		var lightning = new THREE.LightningStrike( THREE.LightningStrike.copyParameters( {}, this.lightningParameters ) );
 		var mesh = new THREE.Mesh( lightning, this.lightningMaterial );
@@ -155,9 +154,9 @@ THREE.LightningStorm.prototype.update = function ( time ) {
 
 	}
 
-	var i = 0; il = this.lightningsMeshes.length;
+	var i = 0, il = this.lightningsMeshes.length;
 
-	while ( i < il ){
+	while ( i < il ) {
 
 		var mesh = this.lightningsMeshes[ i ];
 
@@ -181,18 +180,17 @@ THREE.LightningStorm.prototype.update = function ( time ) {
 
 			// Lightning is to be destroyed
 
-			this.lightningsMeshes.splice( this.lightningsMeshes.indexOf( mesh ), 1 ); 
+			this.lightningsMeshes.splice( this.lightningsMeshes.indexOf( mesh ), 1 );
 
 			this.deadLightningsMeshes.push( mesh );
 
 			this.remove( mesh );
 
-			il--;
+			il --;
 
-		}
-		else {
+		} else {
 
-			i++;
+			i ++;
 
 		}
 
@@ -207,8 +205,8 @@ THREE.LightningStorm.prototype.getNextLightningTime = function ( currentTime ) {
 };
 
 THREE.LightningStorm.prototype.copy = function ( source ) {
-	
-	Object3D.prototype.copy.call( this, source );
+
+	THREE.Object3D.prototype.copy.call( this, source );
 
 	this.stormParams.size = source.stormParams.size;
 	this.stormParams.minHeight = source.stormParams.minHeight;

+ 1 - 2
examples/js/objects/Sky.js

@@ -133,8 +133,7 @@ THREE.Sky.SkyShader = {
 		'const float pi = 3.141592653589793238462643383279502884197169;',
 
 		'const float n = 1.0003;', // refractive index of air
-		'const float N = 2.545E25;', // number of molecules per unit volume for air at
-									// 288.15K and 1013mb (sea level -45 celsius)
+		'const float N = 2.545E25;', // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
 
 		// optical length at zenith for molecules
 		'const float rayleighZenithLength = 8.4E3;',

+ 1 - 0
examples/js/offscreen/scene.js

@@ -15,6 +15,7 @@ function init( canvas, width, height, pixelRatio, path ) {
 	// we don't use ImageLoader since it has a DOM dependency (HTML5 image element)
 
 	var loader = new THREE.ImageBitmapLoader().setPath( path );
+	loader.setOptions( { imageOrientation: 'flipY' } );
 	loader.load( 'textures/matcaps/matcap-porcelain-white.jpg', function ( imageBitmap ) {
 
 		var texture = new THREE.CanvasTexture( imageBitmap );

+ 11 - 16
examples/js/postprocessing/AdaptiveToneMappingPass.js

@@ -117,12 +117,7 @@ THREE.AdaptiveToneMappingPass = function ( adaptive, resolution ) {
 		blending: THREE.NoBlending
 	} );
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -146,35 +141,35 @@ THREE.AdaptiveToneMappingPass.prototype = Object.assign( Object.create( THREE.Pa
 		if ( this.adaptive ) {
 
 			//Render the luminance of the current scene into a render target with mipmapping enabled
-			this.quad.material = this.materialLuminance;
+			this.fsQuad.material = this.materialLuminance;
 			this.materialLuminance.uniforms.tDiffuse.value = readBuffer.texture;
 			renderer.setRenderTarget( this.currentLuminanceRT );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			//Use the new luminance values, the previous luminance and the frame delta to
 			//adapt the luminance over time.
-			this.quad.material = this.materialAdaptiveLum;
+			this.fsQuad.material = this.materialAdaptiveLum;
 			this.materialAdaptiveLum.uniforms.delta.value = deltaTime;
 			this.materialAdaptiveLum.uniforms.lastLum.value = this.previousLuminanceRT.texture;
 			this.materialAdaptiveLum.uniforms.currentLum.value = this.currentLuminanceRT.texture;
 			renderer.setRenderTarget( this.luminanceRT );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			//Copy the new adapted luminance value so that it can be used by the next frame.
-			this.quad.material = this.materialCopy;
+			this.fsQuad.material = this.materialCopy;
 			this.copyUniforms.tDiffuse.value = this.luminanceRT.texture;
 			renderer.setRenderTarget( this.previousLuminanceRT );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 
-		this.quad.material = this.materialToneMap;
+		this.fsQuad.material = this.materialToneMap;
 		this.materialToneMap.uniforms.tDiffuse.value = readBuffer.texture;
 
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
@@ -182,7 +177,7 @@ THREE.AdaptiveToneMappingPass.prototype = Object.assign( Object.create( THREE.Pa
 
 			if ( this.clear ) renderer.clear();
 
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 
@@ -230,7 +225,7 @@ THREE.AdaptiveToneMappingPass.prototype = Object.assign( Object.create( THREE.Pa
 
 		}
 		//Put something in the adaptive luminance texture so that the scene can render initially
-		this.quad.material = new THREE.MeshBasicMaterial( { color: 0x777777 } );
+		this.fsQuad.material = new THREE.MeshBasicMaterial( { color: 0x777777 } );
 		this.materialLuminance.needsUpdate = true;
 		this.materialAdaptiveLum.needsUpdate = true;
 		this.materialToneMap.needsUpdate = true;

+ 20 - 23
examples/js/postprocessing/AfterimagePass.js

@@ -39,23 +39,10 @@ THREE.AfterimagePass = function ( damp ) {
 
 	} );
 
-	this.sceneComp = new THREE.Scene();
-	this.scene = new THREE.Scene();
+	this.compFsQuad = new THREE.Pass.FullScreenQuad( this.shaderMaterial );
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.camera.position.z = 1;
-
-	var geometry = new THREE.PlaneBufferGeometry( 2, 2 );
-
-	this.quadComp = new THREE.Mesh( geometry, this.shaderMaterial );
-	this.sceneComp.add( this.quadComp );
-
-	var material = new THREE.MeshBasicMaterial( {
-		map: this.textureComp.texture
-	} );
-
-	var quadScreen = new THREE.Mesh( geometry, material );
-	this.scene.add( quadScreen );
+	var material = new THREE.MeshBasicMaterial();
+	this.copyFsQuad = new THREE.Pass.FullScreenQuad( material );
 
 };
 
@@ -68,18 +55,15 @@ THREE.AfterimagePass.prototype = Object.assign( Object.create( THREE.Pass.protot
 		this.uniforms[ "tOld" ].value = this.textureOld.texture;
 		this.uniforms[ "tNew" ].value = readBuffer.texture;
 
-		this.quadComp.material = this.shaderMaterial;
-
 		renderer.setRenderTarget( this.textureComp );
-		renderer.render( this.sceneComp, this.camera );
+		this.compFsQuad.render( renderer );
 
-		renderer.setRenderTarget( this.textureOld );
-		renderer.render( this.scene, this.camera );
+		this.copyFsQuad.material.map = this.textureComp.texture;
 
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.copyFsQuad.render( renderer );
 
 		} else {
 
@@ -87,10 +71,23 @@ THREE.AfterimagePass.prototype = Object.assign( Object.create( THREE.Pass.protot
 
 			if ( this.clear ) renderer.clear();
 
-			renderer.render( this.scene, this.camera );
+			this.copyFsQuad.render( renderer );
 
 		}
 
+		// Swap buffers.
+		var temp = this.textureOld;
+		this.textureOld = this.textureComp;
+		this.textureComp = temp;
+		// Now textureOld contains the latest image, ready for the next frame.
+
+	},
+
+	setSize: function ( width, height ) {
+
+		this.textureComp.setSize( width, height );
+		this.textureOld.setSize( width, height );
+
 	}
 
 } );

+ 6 - 11
examples/js/postprocessing/BloomPass.js

@@ -67,12 +67,7 @@ THREE.BloomPass = function ( strength, kernelSize, sigma, resolution ) {
 
 	this.needsSwap = false;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -86,14 +81,14 @@ THREE.BloomPass.prototype = Object.assign( Object.create( THREE.Pass.prototype )
 
 		// Render quad with blured scene into texture (convolution pass 1)
 
-		this.quad.material = this.materialConvolution;
+		this.fsQuad.material = this.materialConvolution;
 
 		this.convolutionUniforms[ "tDiffuse" ].value = readBuffer.texture;
 		this.convolutionUniforms[ "uImageIncrement" ].value = THREE.BloomPass.blurX;
 
 		renderer.setRenderTarget( this.renderTargetX );
 		renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 
 		// Render quad with blured scene into texture (convolution pass 2)
@@ -103,11 +98,11 @@ THREE.BloomPass.prototype = Object.assign( Object.create( THREE.Pass.prototype )
 
 		renderer.setRenderTarget( this.renderTargetY );
 		renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		// Render original scene with superimposed blur to texture
 
-		this.quad.material = this.materialCopy;
+		this.fsQuad.material = this.materialCopy;
 
 		this.copyUniforms[ "tDiffuse" ].value = this.renderTargetY.texture;
 
@@ -115,7 +110,7 @@ THREE.BloomPass.prototype = Object.assign( Object.create( THREE.Pass.prototype )
 
 		renderer.setRenderTarget( readBuffer );
 		if ( this.clear ) renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 	}
 

+ 3 - 10
examples/js/postprocessing/BokehPass.js

@@ -66,12 +66,7 @@ THREE.BokehPass = function ( scene, camera, params ) {
 	this.uniforms = bokehUniforms;
 	this.needsSwap = false;
 
-	this.camera2 = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene2 = new THREE.Scene();
-
-	this.quad2 = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad2.frustumCulled = false; // Avoid getting clipped
-	this.scene2.add( this.quad2 );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.materialBokeh );
 
 	this.oldClearColor = new THREE.Color();
 	this.oldClearAlpha = 1;
@@ -84,8 +79,6 @@ THREE.BokehPass.prototype = Object.assign( Object.create( THREE.Pass.prototype )
 
 	render: function ( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
 
-		this.quad2.material = this.materialBokeh;
-
 		// Render depth into texture
 
 		this.scene.overrideMaterial = this.materialDepth;
@@ -110,13 +103,13 @@ THREE.BokehPass.prototype = Object.assign( Object.create( THREE.Pass.prototype )
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene2, this.camera2 );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			renderer.clear();
-			renderer.render( this.scene2, this.camera2 );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 10
examples/js/postprocessing/DotScreenPass.js

@@ -25,12 +25,7 @@ THREE.DotScreenPass = function ( center, angle, scale ) {
 
 	} );
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 
 };
 
@@ -43,18 +38,16 @@ THREE.DotScreenPass.prototype = Object.assign( Object.create( THREE.Pass.prototy
 		this.uniforms[ "tDiffuse" ].value = readBuffer.texture;
 		this.uniforms[ "tSize" ].value.set( readBuffer.width, readBuffer.height );
 
-		this.quad.material = this.material;
-
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			if ( this.clear ) renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 62 - 1
examples/js/postprocessing/EffectComposer.js

@@ -28,6 +28,8 @@ THREE.EffectComposer = function ( renderer, renderTarget ) {
 	this.writeBuffer = this.renderTarget1;
 	this.readBuffer = this.renderTarget2;
 
+	this.renderToScreen = true;
+
 	this.passes = [];
 
 	// dependencies
@@ -75,6 +77,22 @@ Object.assign( THREE.EffectComposer.prototype, {
 
 	},
 
+	isLastEnabledPass: function ( passIndex ) {
+
+		for ( var i = passIndex + 1; i < this.passes.length; i ++ ) {
+
+			if ( this.passes[ i ].enabled ) {
+
+				return false;
+
+			}
+
+		}
+
+		return true;
+
+	},
+
 	render: function ( deltaTime ) {
 
 		// deltaTime value is in seconds
@@ -99,6 +117,7 @@ Object.assign( THREE.EffectComposer.prototype, {
 
 			if ( pass.enabled === false ) continue;
 
+			pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
 			pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
 
 			if ( pass.needsSwap ) {
@@ -187,7 +206,7 @@ THREE.Pass = function () {
 	// if set to true, the pass clears its buffer before rendering
 	this.clear = false;
 
-	// if set to true, the result of the pass is rendered to screen
+	// if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
 	this.renderToScreen = false;
 
 };
@@ -203,3 +222,45 @@ Object.assign( THREE.Pass.prototype, {
 	}
 
 } );
+
+// Helper for passes that need to fill the viewport with a single quad.
+THREE.Pass.FullScreenQuad = ( function () {
+
+	var camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+	var geometry = new THREE.PlaneBufferGeometry( 2, 2 );
+
+	var FullScreenQuad = function ( material ) {
+
+		this._mesh = new THREE.Mesh( geometry, material );
+
+	};
+
+	Object.defineProperty( FullScreenQuad.prototype, 'material', {
+
+		get: function () {
+
+			return this._mesh.material;
+
+		},
+
+		set: function ( value ) {
+
+			this._mesh.material = value;
+
+		}
+
+	} );
+
+	Object.assign( FullScreenQuad.prototype, {
+
+		render: function ( renderer ) {
+
+			renderer.render( this._mesh, camera );
+
+		}
+
+	} );
+
+	return FullScreenQuad;
+
+} )();

+ 3 - 10
examples/js/postprocessing/FilmPass.js

@@ -26,12 +26,7 @@ THREE.FilmPass = function ( noiseIntensity, scanlinesIntensity, scanlinesCount,
 	if ( scanlinesIntensity !== undefined ) this.uniforms.sIntensity.value = scanlinesIntensity;
 	if ( scanlinesCount !== undefined ) this.uniforms.sCount.value = scanlinesCount;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 
 };
 
@@ -44,18 +39,16 @@ THREE.FilmPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ),
 		this.uniforms[ "tDiffuse" ].value = readBuffer.texture;
 		this.uniforms[ "time" ].value += deltaTime;
 
-		this.quad.material = this.material;
-
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			if ( this.clear ) renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 9
examples/js/postprocessing/GlitchPass.js

@@ -23,12 +23,7 @@ THREE.GlitchPass = function ( dt_size ) {
 		fragmentShader: shader.fragmentShader
 	} );
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 
 	this.goWild = false;
 	this.curF = 0;
@@ -73,18 +68,17 @@ THREE.GlitchPass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 		}
 
 		this.curF ++;
-		this.quad.material = this.material;
 
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			if ( this.clear ) renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 8
examples/js/postprocessing/HalftonePass.js

@@ -36,11 +36,7 @@ THREE.HalftonePass = function ( width, height, params ) {
 
 	}
 
- 	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
- 	this.scene = new THREE.Scene();
- 	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
- 	this.quad.frustumCulled = false;
- 	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 
 };
 
@@ -51,18 +47,17 @@ THREE.HalftonePass.prototype = Object.assign( Object.create( THREE.Pass.prototyp
 	render: function ( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
 
  		this.material.uniforms[ "tDiffuse" ].value = readBuffer.texture;
- 		this.quad.material = this.material;
 
  		if ( this.renderToScreen ) {
 
  			renderer.setRenderTarget( null );
- 			renderer.render( this.scene, this.camera );
+ 			this.fsQuad.render( renderer );
 
 		} else {
 
  			renderer.setRenderTarget( writeBuffer );
  			if ( this.clear ) renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 15 - 20
examples/js/postprocessing/OutlinePass.js

@@ -101,12 +101,7 @@ THREE.OutlinePass = function ( resolution, scene, camera, selectedObjects ) {
 	this.oldClearColor = new THREE.Color();
 	this.oldClearAlpha = 1;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 	this.tempPulseColor1 = new THREE.Color();
 	this.tempPulseColor2 = new THREE.Color();
@@ -302,11 +297,11 @@ THREE.OutlinePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 			this.renderScene.background = currentBackground;
 
 			// 2. Downsample to Half resolution
-			this.quad.material = this.materialCopy;
+			this.fsQuad.material = this.materialCopy;
 			this.copyUniforms[ "tDiffuse" ].value = this.renderTargetMaskBuffer.texture;
 			renderer.setRenderTarget( this.renderTargetMaskDownSampleBuffer );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			this.tempPulseColor1.copy( this.visibleEdgeColor );
 			this.tempPulseColor2.copy( this.hiddenEdgeColor );
@@ -320,44 +315,44 @@ THREE.OutlinePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 			}
 
 			// 3. Apply Edge Detection Pass
-			this.quad.material = this.edgeDetectionMaterial;
+			this.fsQuad.material = this.edgeDetectionMaterial;
 			this.edgeDetectionMaterial.uniforms[ "maskTexture" ].value = this.renderTargetMaskDownSampleBuffer.texture;
 			this.edgeDetectionMaterial.uniforms[ "texSize" ].value = new THREE.Vector2( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );
 			this.edgeDetectionMaterial.uniforms[ "visibleEdgeColor" ].value = this.tempPulseColor1;
 			this.edgeDetectionMaterial.uniforms[ "hiddenEdgeColor" ].value = this.tempPulseColor2;
 			renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			// 4. Apply Blur on Half res
-			this.quad.material = this.separableBlurMaterial1;
+			this.fsQuad.material = this.separableBlurMaterial1;
 			this.separableBlurMaterial1.uniforms[ "colorTexture" ].value = this.renderTargetEdgeBuffer1.texture;
 			this.separableBlurMaterial1.uniforms[ "direction" ].value = THREE.OutlinePass.BlurDirectionX;
 			this.separableBlurMaterial1.uniforms[ "kernelRadius" ].value = this.edgeThickness;
 			renderer.setRenderTarget( this.renderTargetBlurBuffer1 );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 			this.separableBlurMaterial1.uniforms[ "colorTexture" ].value = this.renderTargetBlurBuffer1.texture;
 			this.separableBlurMaterial1.uniforms[ "direction" ].value = THREE.OutlinePass.BlurDirectionY;
 			renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			// Apply Blur on quarter res
-			this.quad.material = this.separableBlurMaterial2;
+			this.fsQuad.material = this.separableBlurMaterial2;
 			this.separableBlurMaterial2.uniforms[ "colorTexture" ].value = this.renderTargetEdgeBuffer1.texture;
 			this.separableBlurMaterial2.uniforms[ "direction" ].value = THREE.OutlinePass.BlurDirectionX;
 			renderer.setRenderTarget( this.renderTargetBlurBuffer2 );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 			this.separableBlurMaterial2.uniforms[ "colorTexture" ].value = this.renderTargetBlurBuffer2.texture;
 			this.separableBlurMaterial2.uniforms[ "direction" ].value = THREE.OutlinePass.BlurDirectionY;
 			renderer.setRenderTarget( this.renderTargetEdgeBuffer2 );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			// Blend it additively over the input texture
-			this.quad.material = this.overlayMaterial;
+			this.fsQuad.material = this.overlayMaterial;
 			this.overlayMaterial.uniforms[ "maskTexture" ].value = this.renderTargetMaskBuffer.texture;
 			this.overlayMaterial.uniforms[ "edgeTexture1" ].value = this.renderTargetEdgeBuffer1.texture;
 			this.overlayMaterial.uniforms[ "edgeTexture2" ].value = this.renderTargetEdgeBuffer2.texture;
@@ -370,7 +365,7 @@ THREE.OutlinePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 			if ( maskActive ) renderer.context.enable( renderer.context.STENCIL_TEST );
 
 			renderer.setRenderTarget( readBuffer );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			renderer.setClearColor( this.oldClearColor, this.oldClearAlpha );
 			renderer.autoClear = oldAutoClear;
@@ -379,10 +374,10 @@ THREE.OutlinePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 
 		if ( this.renderToScreen ) {
 
-			this.quad.material = this.materialCopy;
+			this.fsQuad.material = this.materialCopy;
 			this.copyUniforms[ "tDiffuse" ].value = readBuffer.texture;
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 6
examples/js/postprocessing/SAOPass.js

@@ -160,10 +160,7 @@ THREE.SAOPass = function ( scene, camera, depthTexture, useNormals, resolution )
 		blending: THREE.NoBlending
 	} );
 
-	this.quadCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.quadScene = new THREE.Scene();
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quadScene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -331,8 +328,8 @@ THREE.SAOPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ),
 
 		}
 
-		this.quad.material = passMaterial;
-		renderer.render( this.quadScene, this.quadCamera );
+		this.fsQuad.material = passMaterial;
+		this.fsQuad.render( renderer );
 
 		// restore original state
 		renderer.autoClear = originalAutoClear;

+ 8 - 13
examples/js/postprocessing/SMAAPass.js

@@ -109,12 +109,7 @@ THREE.SMAAPass = function ( width, height ) {
 
 	this.needsSwap = false;
 
-	this.camera = new THREE.OrthographicCamera( -1, 1, 1, -1, 0, 1 );
-	this.scene  = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -128,36 +123,36 @@ THREE.SMAAPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ),
 
 		this.uniformsEdges[ "tDiffuse" ].value = readBuffer.texture;
 
-		this.quad.material = this.materialEdges;
+		this.fsQuad.material = this.materialEdges;
 
 		renderer.setRenderTarget( this.edgesRT );
 		if ( this.clear ) renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		// pass 2
 
-		this.quad.material = this.materialWeights;
+		this.fsQuad.material = this.materialWeights;
 
 		renderer.setRenderTarget( this.weightsRT );
 		if ( this.clear ) renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		// pass 3
 
 		this.uniformsBlend[ "tColor" ].value = readBuffer.texture;
 
-		this.quad.material = this.materialBlend;
+		this.fsQuad.material = this.materialBlend;
 
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			if ( this.clear ) renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 2 - 6
examples/js/postprocessing/SSAARenderPass.js

@@ -40,11 +40,7 @@ THREE.SSAARenderPass = function ( scene, camera, clearColor, clearAlpha ) {
 		depthWrite: false
 	} );
 
-	this.camera2 = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene2	= new THREE.Scene();
-	this.quad2 = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), this.copyMaterial );
-	this.quad2.frustumCulled = false; // Avoid getting clipped
-	this.scene2.add( this.quad2 );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.copyMaterial );
 
 };
 
@@ -133,7 +129,7 @@ THREE.SSAARenderPass.prototype = Object.assign( Object.create( THREE.Pass.protot
 
 			}
 
-			renderer.render( this.scene2, this.camera2 );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 10
examples/js/postprocessing/SSAOPass.js

@@ -134,14 +134,7 @@ THREE.SSAOPass = function ( scene, camera, width, height ) {
 		blendEquationAlpha: THREE.AddEquation
 	} );
 
-	//
-
-	this.quadCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.quadScene = new THREE.Scene();
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quadScene.add( this.quad );
-
-	//
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 	this.originalClearColor = new THREE.Color();
 
@@ -276,8 +269,8 @@ THREE.SSAOPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ),
 
 		}
 
-		this.quad.material = passMaterial;
-		renderer.render( this.quadScene, this.quadCamera );
+		this.fsQuad.material = passMaterial;
+		this.fsQuad.render( renderer );
 
 		// restore original state
 		renderer.autoClear = originalAutoClear;

+ 2 - 9
examples/js/postprocessing/SavePass.js

@@ -34,12 +34,7 @@ THREE.SavePass = function ( renderTarget ) {
 
 	this.needsSwap = false;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 
 };
 
@@ -55,11 +50,9 @@ THREE.SavePass.prototype = Object.assign( Object.create( THREE.Pass.prototype ),
 
 		}
 
-		this.quad.material = this.material;
-
 		renderer.setRenderTarget( this.renderTarget );
 		if ( this.clear ) renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 	}
 

+ 4 - 10
examples/js/postprocessing/ShaderPass.js

@@ -29,13 +29,7 @@ THREE.ShaderPass = function ( shader, textureID ) {
 
 	}
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
-
+	this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
 };
 
 THREE.ShaderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), {
@@ -50,19 +44,19 @@ THREE.ShaderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 
 		}
 
-		this.quad.material = this.material;
+		this.fsQuad.material = this.material;
 
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( writeBuffer );
 			// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
 			if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 3
examples/js/postprocessing/TAARenderPass.js

@@ -99,7 +99,7 @@ THREE.TAARenderPass.prototype = Object.assign( Object.create( THREE.SSAARenderPa
 
 				renderer.setRenderTarget( this.sampleRenderTarget );
 				if ( this.accumulateIndex === 0 ) renderer.clear();
-				renderer.render( this.scene2, this.camera2 );
+				this.fsQuad.render( renderer );
 
 				this.accumulateIndex ++;
 
@@ -119,7 +119,7 @@ THREE.TAARenderPass.prototype = Object.assign( Object.create( THREE.SSAARenderPa
 			this.copyUniforms[ "tDiffuse" ].value = this.sampleRenderTarget.texture;
 			renderer.setRenderTarget( writeBuffer );
 			renderer.clear();
-			renderer.render( this.scene2, this.camera2 );
+			this.fsQuad.render( renderer );
 
 		}
 
@@ -129,7 +129,7 @@ THREE.TAARenderPass.prototype = Object.assign( Object.create( THREE.SSAARenderPa
 			this.copyUniforms[ "tDiffuse" ].value = this.holdRenderTarget.texture;
 			renderer.setRenderTarget( writeBuffer );
 			if ( accumulationWeight === 0 ) renderer.clear();
-			renderer.render( this.scene2, this.camera2 );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 3 - 8
examples/js/postprocessing/TexturePass.js

@@ -28,12 +28,7 @@ THREE.TexturePass = function ( map, opacity ) {
 
 	this.needsSwap = false;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -46,7 +41,7 @@ THREE.TexturePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 		var oldAutoClear = renderer.autoClear;
 		renderer.autoClear = false;
 
-		this.quad.material = this.material;
+		this.fsQuad.material = this.material;
 
 		this.uniforms[ "opacity" ].value = this.opacity;
 		this.uniforms[ "tDiffuse" ].value = this.map;
@@ -54,7 +49,7 @@ THREE.TexturePass.prototype = Object.assign( Object.create( THREE.Pass.prototype
 
 		renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
 		if ( this.clear ) renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		renderer.autoClear = oldAutoClear;
 	}

+ 13 - 18
examples/js/postprocessing/UnrealBloomPass.js

@@ -131,14 +131,9 @@ THREE.UnrealBloomPass = function ( resolution, strength, radius, threshold ) {
 	this.oldClearColor = new THREE.Color();
 	this.oldClearAlpha = 1;
 
-	this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
-	this.scene = new THREE.Scene();
-
 	this.basic = new THREE.MeshBasicMaterial();
 
-	this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null );
-	this.quad.frustumCulled = false; // Avoid getting clipped
-	this.scene.add( this.quad );
+	this.fsQuad = new THREE.Pass.FullScreenQuad( null );
 
 };
 
@@ -200,12 +195,12 @@ THREE.UnrealBloomPass.prototype = Object.assign( Object.create( THREE.Pass.proto
 
 		if ( this.renderToScreen ) {
 
-			this.quad.material = this.basic;
+			this.fsQuad.material = this.basic;
 			this.basic.map = readBuffer.texture;
 
 			renderer.setRenderTarget( null );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 
@@ -213,11 +208,11 @@ THREE.UnrealBloomPass.prototype = Object.assign( Object.create( THREE.Pass.proto
 
 		this.highPassUniforms[ "tDiffuse" ].value = readBuffer.texture;
 		this.highPassUniforms[ "luminosityThreshold" ].value = this.threshold;
-		this.quad.material = this.materialHighPassFilter;
+		this.fsQuad.material = this.materialHighPassFilter;
 
 		renderer.setRenderTarget( this.renderTargetBright );
 		renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		// 2. Blur All the mips progressively
 
@@ -225,19 +220,19 @@ THREE.UnrealBloomPass.prototype = Object.assign( Object.create( THREE.Pass.proto
 
 		for ( var i = 0; i < this.nMips; i ++ ) {
 
-			this.quad.material = this.separableBlurMaterials[ i ];
+			this.fsQuad.material = this.separableBlurMaterials[ i ];
 
 			this.separableBlurMaterials[ i ].uniforms[ "colorTexture" ].value = inputRenderTarget.texture;
 			this.separableBlurMaterials[ i ].uniforms[ "direction" ].value = THREE.UnrealBloomPass.BlurDirectionX;
 			renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			this.separableBlurMaterials[ i ].uniforms[ "colorTexture" ].value = this.renderTargetsHorizontal[ i ].texture;
 			this.separableBlurMaterials[ i ].uniforms[ "direction" ].value = THREE.UnrealBloomPass.BlurDirectionY;
 			renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
 			renderer.clear();
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 			inputRenderTarget = this.renderTargetsVertical[ i ];
 
@@ -245,18 +240,18 @@ THREE.UnrealBloomPass.prototype = Object.assign( Object.create( THREE.Pass.proto
 
 		// Composite All the mips
 
-		this.quad.material = this.compositeMaterial;
+		this.fsQuad.material = this.compositeMaterial;
 		this.compositeMaterial.uniforms[ "bloomStrength" ].value = this.strength;
 		this.compositeMaterial.uniforms[ "bloomRadius" ].value = this.radius;
 		this.compositeMaterial.uniforms[ "bloomTintColors" ].value = this.bloomTintColors;
 
 		renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
 		renderer.clear();
-		renderer.render( this.scene, this.camera );
+		this.fsQuad.render( renderer );
 
 		// Blend it additively over the input texture
 
-		this.quad.material = this.materialCopy;
+		this.fsQuad.material = this.materialCopy;
 		this.copyUniforms[ "tDiffuse" ].value = this.renderTargetsHorizontal[ 0 ].texture;
 
 		if ( maskActive ) renderer.context.enable( renderer.context.STENCIL_TEST );
@@ -265,12 +260,12 @@ THREE.UnrealBloomPass.prototype = Object.assign( Object.create( THREE.Pass.proto
 		if ( this.renderToScreen ) {
 
 			renderer.setRenderTarget( null );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		} else {
 
 			renderer.setRenderTarget( readBuffer );
-			renderer.render( this.scene, this.camera );
+			this.fsQuad.render( renderer );
 
 		}
 

+ 2 - 1
examples/js/renderers/CSS2DRenderer.js

@@ -9,7 +9,7 @@ THREE.CSS2DObject = function ( element ) {
 	this.element = element;
 	this.element.style.position = 'absolute';
 
-	this.addEventListener( 'removed', function ( event ) {
+	this.addEventListener( 'removed', function () {
 
 		if ( this.element.parentNode !== null ) {
 
@@ -82,6 +82,7 @@ THREE.CSS2DRenderer = function () {
 			element.style.MozTransform = style;
 			element.style.oTransform = style;
 			element.style.transform = style;
+			element.style.display = ( vector.z < - 1 || vector.z > 1 ) ? 'none' : '';
 
 			var objectData = {
 				distanceToCameraSquared: getDistanceToSquared( camera, object )

+ 3 - 3
examples/js/renderers/Projector.js

@@ -322,7 +322,7 @@ THREE.Projector = function () {
 
 				_face.material = material;
 
-				if ( material.vertexColors === THREE.FaceColors ||  material.vertexColors === THREE.VertexColors ) {
+				if ( material.vertexColors === THREE.FaceColors || material.vertexColors === THREE.VertexColors ) {
 
 					_face.color.fromArray( colors, a * 3 );
 
@@ -1038,8 +1038,8 @@ THREE.Projector = function () {
 
 		var alpha1 = 0, alpha2 = 1,
 
-		// Calculate the boundary coordinate of each vertex for the near and far clip planes,
-		// Z = -1 and Z = +1, respectively.
+			// Calculate the boundary coordinate of each vertex for the near and far clip planes,
+			// Z = -1 and Z = +1, respectively.
 
 			bc1near = s1.z + s1.w,
 			bc2near = s2.z + s2.w,

+ 4 - 37
examples/js/renderers/WebGLDeferredRenderer.js

@@ -182,6 +182,7 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 		rt.texture.generateMipamps = false;
 
 		_compNormalDepth = new THREE.EffectComposer( _this.renderer, rt );
+		_compNormalDepth.renderToScreen = false;
 		_compNormalDepth.addPass( _passNormalDepth );
 
 	}
@@ -202,6 +203,7 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 		rt.texture.generateMipamps = false;
 
 		_compColor = new THREE.EffectComposer( _this.renderer, rt );
+		_compColor.renderToScreen = false;
 		_compColor.addPass( _passColor );
 
 	}
@@ -226,6 +228,7 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 		rt.texture.generateMipamps = false;
 
 		_compLight = new THREE.EffectComposer( _this.renderer, rt );
+		_compLight.renderToScreen = false;
 		_compLight.addPass( _passLightFullscreen );
 		_compLight.addPass( _passLight );
 
@@ -247,6 +250,7 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 		rt.texture.generateMipamps = false;
 
 		_compReconstruction = new THREE.EffectComposer( _this.renderer, rt );
+		_compReconstruction.renderToScreen = false;
 		_compReconstruction.addPass( _passReconstruction );
 
 	}
@@ -1068,24 +1072,15 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 
 		if ( _lightPrePass ) {
 
-			_passForward.renderToScreen = false;
 			_passForward.enabled = false;
-
-			_passCopy.renderToScreen = false;
 			_passCopy.enabled = false;
 
 			if ( _antialias ) {
 
-				_passFinal.renderToScreen = false;
-
-				_passFXAA.renderToScreen = true;
 				_passFXAA.enabled = true;
 
 			} else {
 
-				_passFinal.renderToScreen = true;
-
-				_passFXAA.renderToScreen = false;
 				_passFXAA.enabled = false;
 
 			}
@@ -1096,28 +1091,14 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 
 				if ( _antialias ) {
 
-					_passFinal.renderToScreen = false;
-
-					_passForward.renderToScreen = false;
 					_passForward.enabled = true;
-
-					_passCopy.renderToScreen = false;
 					_passCopy.enabled = false;
-
-					_passFXAA.renderToScreen = true;
 					_passFXAA.enabled = true;
 
 				} else {
 
-					_passFinal.renderToScreen = false;
-
-					_passForward.renderToScreen = false;
 					_passForward.enabled = true;
-
-					_passCopy.renderToScreen = true;
 					_passCopy.enabled = true;
-
-					_passFXAA.renderToScreen = false;
 					_passFXAA.enabled = false;
 
 				}
@@ -1126,28 +1107,14 @@ THREE.WebGLDeferredRenderer = function ( parameters ) {
 
 				if ( _antialias ) {
 
-					_passFinal.renderToScreen = false;
-
-					_passForward.renderToScreen = false;
 					_passForward.enabled = false;
-
-					_passCopy.renderToScreen = false;
 					_passCopy.enabled = false;
-
-					_passFXAA.renderToScreen = true;
 					_passFXAA.enabled = true;
 
 				} else {
 
-					_passFinal.renderToScreen = true;
-
-					_passForward.renderToScreen = false;
 					_passForward.enabled = false;
-
-					_passCopy.renderToScreen = false;
 					_passCopy.enabled = false;
-
-					_passFXAA.renderToScreen = false;
 					_passFXAA.enabled = false;
 
 				}

+ 2 - 1
examples/js/utils/BufferGeometryUtils.js

@@ -498,7 +498,8 @@ THREE.BufferGeometryUtils = {
 		var getters = [ 'getX', 'getY', 'getZ', 'getW' ];
 
 		// initialize the arrays
-		for ( var name of attributeNames ) {
+		for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
+			var name = attributeNames[ i ];
 
 			attrArrays[ name ] = [];
 

+ 52 - 0
examples/js/utils/SkeletonUtils.js

@@ -528,6 +528,58 @@ THREE.SkeletonUtils = {
 
 		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 );
+
+	}
+
+}

+ 0 - 0
examples/js/TypedArrayUtils.js → examples/js/utils/TypedArrayUtils.js


+ 2 - 2
examples/js/utils/UVsDebug.js

@@ -83,7 +83,7 @@ THREE.UVsDebug = function ( geometry, size ) {
 				uvs[ 1 ].fromBufferAttribute( uvAttribute, face[ 1 ] );
 				uvs[ 2 ].fromBufferAttribute( uvAttribute, face[ 2 ] );
 
-				processFace( face, uvs, i );
+				processFace( face, uvs, i / 3 );
 
 			}
 
@@ -101,7 +101,7 @@ THREE.UVsDebug = function ( geometry, size ) {
 				uvs[ 1 ].fromBufferAttribute( uvAttribute, face[ 1 ] );
 				uvs[ 2 ].fromBufferAttribute( uvAttribute, face[ 2 ] );
 
-				processFace( face, uvs, i );
+				processFace( face, uvs, i / 3 );
 
 			}
 

+ 0 - 300
examples/js/utils/ldraw/packLDrawModel.js

@@ -1,300 +0,0 @@
-/*
- * @author yomboprime / https://github.com/yomboprime/
- *
- * LDraw object packer
- *
- * Usage:
- *
- * - Download official parts library from LDraw.org and unzip in a directory (e.g. ldraw/)
- *
- * - Download your desired model file and place in the ldraw/models/ subfolder.
- *
- * - Place this script also in ldraw/
- *
- * - Issue command 'node packLDrawModel models/<modelFileName>'
- *
- * The packed object will be in ldraw/models/<modelFileName>_Packed.mpd and will contain all the object subtree as embedded files.
- *
- *
- */
-
-var ldrawPath = './';
-var materialsFileName = 'LDConfig.ldr';
-
-
-var fs = require( 'fs' );
-var path = require( 'path' );
-
-if ( process.argv.length !== 3 ) {
-	console.log( "Usage: node packLDrawModel <modelFilePath>" );
-	exit ( 0 );
-}
-var fileName = process.argv[ 2 ];
-
-var materialsFilePath = path.join( ldrawPath, materialsFileName );
-
-console.log( 'Loading materials file "' + materialsFilePath + '"...' );
-var materialsContent = fs.readFileSync( materialsFilePath, { encoding: "utf8" } );
-
-console.log( 'Packing "' + fileName + '"...' );
-
-var objectsPaths = [];
-var objectsContents = [];
-var pathMap = {};
-var listOfNotFound = [];
-
-// Parse object tree
-parseObject( fileName, true );
-
-// Check if previously files not found are found now
-// (if so, probably they were already embedded)
-var someNotFound = false;
-for ( var i = 0; i < listOfNotFound.length; i ++ ) {
-
-	if ( ! pathMap[ listOfNotFound[ i ] ] ) {
-
-		someNotFound = true;
-		console.log( 'Error: File object not found: "' + fileName + '".' );
-
-	}
-
-}
-
-if ( someNotFound ) {
-	console.log( "Some files were not found, aborting." );
-	process.exit( -1 );
-}
-
-// Obtain packed content
-var packedContent = materialsContent + "\n";
-for ( var i = objectsPaths.length - 1; i >= 0; i -- ) {
-	packedContent += objectsContents[ i ];
-}
-packedContent += "\n";
-
-// Save output file
-var outPath = fileName + "_Packed.mpd";
-console.log( 'Writing "' + outPath + '"...' );
-fs.writeFileSync( outPath, packedContent );
-
-console.log( 'Done.' );
-
-
-//
-
-function parseObject( fileName, isRoot ) {
-
-	// Returns the located path for fileName or null if not found
-
-	console.log( 'Adding "' + fileName + '".' );
-
-	var originalFileName = fileName;
-
-	var prefix = "";
-	var objectContent = null;
-	for ( var attempt =  0; attempt < 2; attempt ++ ) {
-
-		prefix = "";
-
-		if ( attempt === 1 ) {
-
-			fileName = fileName.toLowerCase();
-
-		}
-
-		if ( fileName.startsWith( '48/' ) ) {
-
-			prefix = "p/";
-
-		}
-		else if ( fileName.startsWith( 's/' ) ) {
-
-			prefix = "parts/";
-
-		}
-
-		var absoluteObjectPath = path.join( ldrawPath, fileName );
-
-		try {
-
-			objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
-			break;
-
-		}
-		catch ( e ) {
-
-			prefix = "parts/";
-			absoluteObjectPath = path.join( ldrawPath, prefix, fileName );
-
-			try {
-
-				objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
-				break;
-
-			}
-			catch ( e ) {
-
-				prefix = "p/";
-				absoluteObjectPath = path.join( ldrawPath, prefix, fileName );
-
-				try {
-
-					objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
-					break;
-
-				}
-				catch ( e ) {
-
-					try {
-
-						prefix = "models/";
-						absoluteObjectPath = path.join( ldrawPath, prefix, fileName );
-
-						objectContent = fs.readFileSync( absoluteObjectPath, { encoding: "utf8" } );
-						break;
-
-					}
-					catch ( e ) {
-						if ( attempt === 1 ) {
-
-							// The file has not been found, add to list of not found
-							listOfNotFound.push( originalFileName );
-
-						}
-
-					}
-
-				}
-
-			}
-
-		}
-
-	}
-
-	var objectPath = path.join( prefix, fileName );
-
-	if ( ! objectContent ) {
-
-		// File was not found, but could be a referenced embedded file.
-		return null;
-
-	}
-
-	if ( objectContent.indexOf( '\r\n' ) !== - 1 ) {
-
-		// This is faster than String.split with regex that splits on both
-		objectContent= objectContent.replace( /\r\n/g, '\n' );
-
-	}
-
-	var processedObjectContent = isRoot ? "" : "0 FILE " + objectPath + "\n";
-
-	var lines = objectContent.split( "\n" );
-
-	for ( var i = 0, n = lines.length; i < n; i ++ ) {
-
-		var line = lines[ i ];
-		var lineLength = line.length;
-
-		// Skip spaces/tabs
-		var charIndex = 0;
-		while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) {
-
-			charIndex++;
-
-		}
-		line = line.substring( charIndex );
-		lineLength = line.length;
-		charIndex = 0;
-
-
-		if ( line.startsWith( '0 FILE ') ) {
-
-			if ( i === 0 ) {
-
-				// Ignore first line FILE meta directive
-				continue;
-
-			}
-
-			// Embedded object was found, add to path map
-
-			var subobjectFileName = line.substring( charIndex ).trim().replace( "\\", "/" );
-
-			if ( subobjectFileName ) {
-
-				// Find name in path cache
-				var subobjectPath = pathMap[ subobjectFileName ];
-
-				if ( ! subobjectPath ) {
-
-					pathMap[ subobjectFileName ] = subobjectFileName;
-
-				}
-
-			}
-
-		}
-
-		if ( line.startsWith( '1 ' ) ) {
-			// Subobject, add it
-			charIndex = 2;
-
-			// Skip material, position and transform
-			for ( var token = 0; token < 13 && charIndex < lineLength; token ++ ) {
-
-				// Skip token
-				while ( line.charAt( charIndex ) !== ' ' && line.charAt( charIndex ) !== '\t' && charIndex < lineLength ) {
-
-					charIndex++;
-
-				}
-
-				// Skip spaces/tabs
-				while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) {
-
-					charIndex++;
-
-				}
-
-			}
-
-			var subobjectFileName = line.substring( charIndex ).trim().replace( "\\", "/" );
-
-			if ( subobjectFileName ) {
-
-				// Find name in path cache
-				var subobjectPath = pathMap[ subobjectFileName ];
-
-				if ( ! subobjectPath ) {
-
-					// Add new object
-					subobjectPath = parseObject( subobjectFileName );
-
-				}
-
-				pathMap[ subobjectFileName ] = subobjectPath ? subobjectPath : subobjectFileName;
-
-				processedObjectContent += line.substring( 0, charIndex ) + pathMap[ subobjectFileName ] + "\n";
-
-			}
-
-		}
-		else {
-
-			processedObjectContent += line + "\n";
-
-		}
-
-	}
-
-	if ( objectsPaths.indexOf( objectPath ) < 0 ) {
-
-		objectsPaths.push( objectPath );
-		objectsContents.push( processedObjectContent );
-
-	}
-
-	return objectPath;
-}

+ 71 - 0
examples/jsm/controls/MapControls.d.ts

@@ -0,0 +1,71 @@
+import {
+  Camera,
+  EventDispatcher,
+  MOUSE,
+  Object3D,
+  Vector3
+} from '../../../src/Three';
+
+export class MapControls extends EventDispatcher {
+  constructor(object: Camera, domElement?: HTMLElement);
+
+  object: Camera;
+  domElement: HTMLElement | HTMLDocument;
+
+  // API
+  enabled: boolean;
+  target: Vector3;
+
+  enableZoom: boolean;
+  zoomSpeed: number;
+  minDistance: number;
+  maxDistance: number;
+  enableRotate: boolean;
+  rotateSpeed: number;
+  enablePan: boolean;
+  keyPanSpeed: number;
+  maxZoom: number;
+  minZoom: number;
+  panSpeed: number;
+  autoRotate: boolean;
+  autoRotateSpeed: number;
+  minPolarAngle: number;
+  maxPolarAngle: number;
+  minAzimuthAngle: number;
+  maxAzimuthAngle: number;
+  enableKeys: boolean;
+  screenSpacePanning: boolean;
+  keys: { LEFT: number; UP: number; RIGHT: number; BOTTOM: number };
+  mouseButtons: { LEFT: MOUSE; MIDDLE: MOUSE; RIGHT: MOUSE };
+  enableDamping: boolean;
+  dampingFactor: number;
+  target0: Vector3;
+  position0: Vector3;
+  zoom0: number;
+
+  rotateLeft(angle?: number): void;
+
+  rotateUp(angle?: number): void;
+
+  panLeft(distance?: number): void;
+
+  panUp(distance?: number): void;
+
+  pan(deltaX: number, deltaY: number): void;
+
+  dollyIn(dollyScale: number): void;
+
+  dollyOut(dollyScale: number): void;
+
+  saveState(): void;
+
+  update(): boolean;
+
+  reset(): void;
+
+  dispose(): void;
+
+  getPolarAngle(): number;
+
+  getAzimuthalAngle(): number;
+}

+ 8 - 8
examples/jsm/controls/MapControls.js

@@ -7,14 +7,6 @@
  * @author moroine / https://github.com/moroine
  */
 
-// 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
-
 import {
 	EventDispatcher,
 	MOUSE,
@@ -24,6 +16,14 @@ import {
 	Vector3
 } from "../../../build/three.module.js";
 
+// 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 ) {
 
 	this.object = object;

+ 70 - 0
examples/jsm/controls/OrbitControls.d.ts

@@ -0,0 +1,70 @@
+import { Camera, MOUSE, Object3D, Vector3 } from '../../../src/Three';
+
+export class OrbitControls {
+  constructor(object: Camera, domElement?: HTMLElement);
+
+  object: Camera;
+  domElement: HTMLElement | HTMLDocument;
+
+  // API
+  enabled: boolean;
+  target: Vector3;
+
+  // deprecated
+  center: Vector3;
+
+  enableZoom: boolean;
+  zoomSpeed: number;
+  minDistance: number;
+  maxDistance: number;
+  enableRotate: boolean;
+  rotateSpeed: number;
+  enablePan: boolean;
+  keyPanSpeed: number;
+  autoRotate: boolean;
+  autoRotateSpeed: number;
+  minPolarAngle: number;
+  maxPolarAngle: number;
+  minAzimuthAngle: number;
+  maxAzimuthAngle: number;
+  enableKeys: boolean;
+  keys: {LEFT: number; UP: number; RIGHT: number; BOTTOM: number;};
+  mouseButtons: {ORBIT: MOUSE; ZOOM: MOUSE; PAN: MOUSE;};
+  enableDamping: boolean;
+  dampingFactor: number;
+  screenSpacePanning: boolean;
+
+
+  rotateLeft(angle?: number): void;
+
+  rotateUp(angle?: number): void;
+
+  panLeft(distance?: number): void;
+
+  panUp(distance?: number): void;
+
+  pan(deltaX: number, deltaY: number): void;
+
+  dollyIn(dollyScale: number): void;
+
+  dollyOut(dollyScale: number): void;
+
+  update(): void;
+
+  reset(): void;
+
+  dispose(): void;
+
+  getPolarAngle(): number;
+
+  getAzimuthalAngle(): number;
+
+  // EventDispatcher mixins
+  addEventListener(type: string, listener: (event: any) => void): void;
+
+  hasEventListener(type: string, listener: (event: any) => void): boolean;
+
+  removeEventListener(type: string, listener: (event: any) => void): void;
+
+  dispatchEvent(event: {type: string; target: any;}): void;
+}

+ 31 - 12
examples/jsm/controls/OrbitControls.js

@@ -6,13 +6,6 @@
  * @author erich666 / http://erichaines.com
  */
 
-// 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
-
 import {
 	EventDispatcher,
 	MOUSE,
@@ -22,6 +15,13 @@ import {
 	Vector3
 } from "../../../build/three.module.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;
@@ -546,32 +546,44 @@ var OrbitControls = function ( object, domElement ) {
 
 	function handleKeyDown( event ) {
 
-		//console.log( 'handleKeyDown' );
+		// console.log( 'handleKeyDown' );
+
+		var needsUpdate = false;
 
 		switch ( event.keyCode ) {
 
 			case scope.keys.UP:
 				pan( 0, scope.keyPanSpeed );
-				scope.update();
+				needsUpdate = true;
 				break;
 
 			case scope.keys.BOTTOM:
 				pan( 0, - scope.keyPanSpeed );
-				scope.update();
+				needsUpdate = true;
 				break;
 
 			case scope.keys.LEFT:
 				pan( scope.keyPanSpeed, 0 );
-				scope.update();
+				needsUpdate = true;
 				break;
 
 			case scope.keys.RIGHT:
 				pan( - scope.keyPanSpeed, 0 );
-				scope.update();
+				needsUpdate = true;
 				break;
 
 		}
 
+		if ( needsUpdate ) {
+
+			// prevent the browser from scrolling on cursor keys
+			event.preventDefault();
+
+			scope.update();
+
+		}
+
+
 	}
 
 	function handleTouchStartRotate( event ) {
@@ -682,8 +694,15 @@ var OrbitControls = function ( object, domElement ) {
 
 		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 scope.mouseButtons.LEFT:

+ 47 - 0
examples/jsm/controls/TrackballControls.d.ts

@@ -0,0 +1,47 @@
+import { Camera, EventDispatcher, Vector3 } from '../../../src/Three';
+
+export class TrackballControls extends EventDispatcher {
+  constructor(object: Camera, domElement?: HTMLElement);
+
+  object: Camera;
+  domElement: HTMLElement;
+
+  // API
+  enabled: boolean;
+  screen: {left: number; top: number; width: number; height: number};
+  rotateSpeed: number;
+  zoomSpeed: number;
+  panSpeed: number;
+  noRotate: boolean;
+  noZoom: boolean;
+  noPan: boolean;
+  noRoll: boolean;
+  staticMoving: boolean;
+  dynamicDampingFactor: number;
+  minDistance: number;
+  maxDistance: number;
+  keys: number[];
+
+  target: Vector3;
+  position0: Vector3;
+  target0: Vector3;
+  up0: Vector3;
+
+  update(): void;
+
+  reset(): void;
+
+  dispose(): void;
+
+  checkDistances(): void;
+
+  zoomCamera(): void;
+
+  panCamera(): void;
+
+  rotateCamera(): void;
+
+  handleResize(): void;
+
+  handleEvent(event: any): void;
+}

+ 1 - 1
examples/jsm/controls/TrackballControls.js

@@ -499,7 +499,7 @@ var TrackballControls = function ( object, domElement ) {
 	function touchstart( event ) {
 
 		if ( _this.enabled === false ) return;
-
+		
 		event.preventDefault();
 
 		switch ( event.touches.length ) {

+ 635 - 0
examples/jsm/exporters/ColladaExporter.js

@@ -0,0 +1,635 @@
+/**
+ * @author Garrett Johnson / http://gkjohnson.github.io/
+ * https://github.com/gkjohnson/collada-exporter-js
+ *
+ * Usage:
+ *  var exporter = new ColladaExporter();
+ *
+ *  var data = exporter.parse(mesh);
+ *
+ * Format Definition:
+ *  https://www.khronos.org/collada/
+ */
+
+import {
+	BufferGeometry,
+	Color,
+	DoubleSide,
+	Geometry,
+	Matrix4,
+	Mesh,
+	MeshBasicMaterial,
+	MeshLambertMaterial
+} from "../../../build/three.module.js";
+
+var ColladaExporter = function () {};
+
+ColladaExporter.prototype = {
+
+	constructor: ColladaExporter,
+
+	parse: function ( object, onDone, options = {} ) {
+
+		options = Object.assign( {
+			version: '1.4.1',
+			author: null,
+			textureDirectory: '',
+		}, options );
+
+		if ( options.textureDirectory !== '' ) {
+
+			options.textureDirectory = `${ options.textureDirectory }/`
+				.replace( /\\/g, '/' )
+				.replace( /\/+/g, '/' );
+
+		}
+
+		var version = options.version;
+		if ( version !== '1.4.1' && version !== '1.5.0' ) {
+
+			console.warn( `ColladaExporter : Version ${ version } not supported for export. Only 1.4.1 and 1.5.0.` );
+			return null;
+
+		}
+
+		// Convert the urdf xml into a well-formatted, indented format
+		function format( urdf ) {
+
+			var IS_END_TAG = /^<\//;
+			var IS_SELF_CLOSING = /(\?>$)|(\/>$)/;
+			var HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/;
+
+			var pad = ( ch, num ) => ( num > 0 ? ch + pad( ch, num - 1 ) : '' );
+
+			var tagnum = 0;
+			return urdf
+				.match( /(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g )
+				.map( tag => {
+
+					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && IS_END_TAG.test( tag ) ) {
+
+						tagnum --;
+
+					}
+
+					var res = `${ pad( '  ', tagnum ) }${ tag }`;
+
+					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && ! IS_END_TAG.test( tag ) ) {
+
+						tagnum ++;
+
+					}
+
+					return res;
+
+				} )
+				.join( '\n' );
+
+		}
+
+		// Convert an image into a png format for saving
+		function base64ToBuffer( str ) {
+
+			var b = atob( str );
+			var buf = new Uint8Array( b.length );
+
+			for ( var i = 0, l = buf.length; i < l; i ++ ) {
+
+				buf[ i ] = b.charCodeAt( i );
+
+			}
+
+			return buf;
+
+		}
+
+		var canvas, ctx;
+		function imageToData( image, ext ) {
+
+			canvas = canvas || document.createElement( 'canvas' );
+			ctx = ctx || canvas.getContext( '2d' );
+
+			canvas.width = image.naturalWidth;
+			canvas.height = image.naturalHeight;
+
+			ctx.drawImage( image, 0, 0 );
+
+			// Get the base64 encoded data
+			var base64data = canvas
+				.toDataURL( `image/${ ext }`, 1 )
+				.replace( /^data:image\/(png|jpg);base64,/, '' );
+
+			// Convert to a uint8 array
+			return base64ToBuffer( base64data );
+
+		}
+
+		// gets the attribute array. Generate a new array if the attribute is interleaved
+		var getFuncs = [ 'getX', 'getY', 'getZ', 'getW' ];
+		function attrBufferToArray( attr ) {
+
+			if ( attr.isInterleavedBufferAttribute ) {
+
+				// use the typed array constructor to save on memory
+				var arr = new attr.array.constructor( attr.count * attr.itemSize );
+				var size = attr.itemSize;
+				for ( var i = 0, l = attr.count; i < l; i ++ ) {
+
+					for ( var j = 0; j < size; j ++ ) {
+
+						arr[ i * size + j ] = attr[ getFuncs[ j ] ]( i );
+
+					}
+
+				}
+
+				return arr;
+
+			} else {
+
+				return attr.array;
+
+			}
+
+		}
+
+		// Returns an array of the same type starting at the `st` index,
+		// and `ct` length
+		function subArray( arr, st, ct ) {
+
+			if ( Array.isArray( arr ) ) return arr.slice( st, st + ct );
+			else return new arr.constructor( arr.buffer, st * arr.BYTES_PER_ELEMENT, ct );
+
+		}
+
+		// Returns the string for a geometry's attribute
+		function getAttribute( attr, name, params, type ) {
+
+			var array = attrBufferToArray( attr );
+			var res =
+					`<source id="${ name }">` +
+
+					`<float_array id="${ name }-array" count="${ array.length }">` +
+					array.join( ' ' ) +
+					'</float_array>' +
+
+					'<technique_common>' +
+					`<accessor source="#${ name }-array" count="${ Math.floor( array.length / attr.itemSize ) }" stride="${ attr.itemSize }">` +
+
+					params.map( n => `<param name="${ n }" type="${ type }" />` ).join( '' ) +
+
+					'</accessor>' +
+					'</technique_common>' +
+					'</source>';
+
+			return res;
+
+		}
+
+		// Returns the string for a node's transform information
+		var transMat;
+		function getTransform( o ) {
+
+			// ensure the object's matrix is up to date
+			// before saving the transform
+			o.updateMatrix();
+
+			transMat = transMat || new Matrix4();
+			transMat.copy( o.matrix );
+			transMat.transpose();
+			return `<matrix>${ transMat.toArray().join( ' ' ) }</matrix>`;
+
+		}
+
+		// Process the given piece of geometry into the geometry library
+		// Returns the mesh id
+		function processGeometry( g ) {
+
+			var info = geometryInfo.get( g );
+
+			if ( ! info ) {
+
+				// convert the geometry to bufferGeometry if it isn't already
+				var bufferGeometry = g;
+				if ( bufferGeometry instanceof Geometry ) {
+
+					bufferGeometry = ( new BufferGeometry() ).fromGeometry( bufferGeometry );
+
+				}
+
+				var meshid = `Mesh${ libraryGeometries.length + 1 }`;
+
+				var indexCount =
+					bufferGeometry.index ?
+						bufferGeometry.index.count * bufferGeometry.index.itemSize :
+						bufferGeometry.attributes.position.count;
+
+				var groups =
+					bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ?
+						bufferGeometry.groups :
+						[ { start: 0, count: indexCount, materialIndex: 0 } ];
+
+				var gnode = `<geometry id="${ meshid }" name="${ g.name }"><mesh>`;
+
+				// define the geometry node and the vertices for the geometry
+				var posName = `${ meshid }-position`;
+				var vertName = `${ meshid }-vertices`;
+				gnode += getAttribute( bufferGeometry.attributes.position, posName, [ 'X', 'Y', 'Z' ], 'float' );
+				gnode += `<vertices id="${ vertName }"><input semantic="POSITION" source="#${ posName }" /></vertices>`;
+
+				// NOTE: We're not optimizing the attribute arrays here, so they're all the same length and
+				// can therefore share the same triangle indices. However, MeshLab seems to have trouble opening
+				// models with attributes that share an offset.
+				// MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/
+
+				// serialize normals
+				var triangleInputs = `<input semantic="VERTEX" source="#${ vertName }" offset="0" />`;
+				if ( 'normal' in bufferGeometry.attributes ) {
+
+					var normName = `${ meshid }-normal`;
+					gnode += getAttribute( bufferGeometry.attributes.normal, normName, [ 'X', 'Y', 'Z' ], 'float' );
+					triangleInputs += `<input semantic="NORMAL" source="#${ normName }" offset="0" />`;
+
+				}
+
+				// serialize uvs
+				if ( 'uv' in bufferGeometry.attributes ) {
+
+					var uvName = `${ meshid }-texcoord`;
+					gnode += getAttribute( bufferGeometry.attributes.uv, uvName, [ 'S', 'T' ], 'float' );
+					triangleInputs += `<input semantic="TEXCOORD" source="#${ uvName }" offset="0" set="0" />`;
+
+				}
+
+				// serialize colors
+				if ( 'color' in bufferGeometry.attributes ) {
+
+					var colName = `${ meshid }-color`;
+					gnode += getAttribute( bufferGeometry.attributes.color, colName, [ 'X', 'Y', 'Z' ], 'uint8' );
+					triangleInputs += `<input semantic="COLOR" source="#${ colName }" offset="0" />`;
+
+				}
+
+				var indexArray = null;
+				if ( bufferGeometry.index ) {
+
+					indexArray = attrBufferToArray( bufferGeometry.index );
+
+				} else {
+
+					indexArray = new Array( indexCount );
+					for ( var i = 0, l = indexArray.length; i < l; i ++ ) indexArray[ i ] = i;
+
+				}
+
+				for ( var i = 0, l = groups.length; i < l; i ++ ) {
+
+					var group = groups[ i ];
+					var subarr = subArray( indexArray, group.start, group.count );
+					var polycount = subarr.length / 3;
+					gnode += `<triangles material="MESH_MATERIAL_${ group.materialIndex }" count="${ polycount }">`;
+					gnode += triangleInputs;
+
+					gnode += `<p>${ subarr.join( ' ' ) }</p>`;
+					gnode += '</triangles>';
+
+				}
+
+				gnode += `</mesh></geometry>`;
+
+				libraryGeometries.push( gnode );
+
+				info = { meshid: meshid, bufferGeometry: bufferGeometry };
+				geometryInfo.set( g, info );
+
+			}
+
+			return info;
+
+		}
+
+		// Process the given texture into the image library
+		// Returns the image library
+		function processTexture( tex ) {
+
+			var texid = imageMap.get( tex );
+			if ( texid == null ) {
+
+				texid = `image-${ libraryImages.length + 1 }`;
+
+				var ext = 'png';
+				var name = tex.name || texid;
+				var imageNode = `<image id="${ texid }" name="${ name }">`;
+
+				if ( version === '1.5.0' ) {
+
+					imageNode += `<init_from><ref>${ options.textureDirectory }${ name }.${ ext }</ref></init_from>`;
+
+				} else {
+
+					// version image node 1.4.1
+					imageNode += `<init_from>${ options.textureDirectory }${ name }.${ ext }</init_from>`;
+
+				}
+
+				imageNode += '</image>';
+
+				libraryImages.push( imageNode );
+				imageMap.set( tex, texid );
+				textures.push( {
+					directory: options.textureDirectory,
+					name,
+					ext,
+					data: imageToData( tex.image, ext ),
+					original: tex
+				} );
+
+			}
+
+			return texid;
+
+		}
+
+		// Process the given material into the material and effect libraries
+		// Returns the material id
+		function processMaterial( m ) {
+
+			var matid = materialMap.get( m );
+
+			if ( matid == null ) {
+
+				matid = `Mat${ libraryEffects.length + 1 }`;
+
+				var type = 'phong';
+
+				if ( m instanceof MeshLambertMaterial ) {
+
+					type = 'lambert';
+
+				} else if ( m instanceof MeshBasicMaterial ) {
+
+					type = 'constant';
+
+					if ( m.map !== null ) {
+
+						// The Collada spec does not support diffuse texture maps with the
+						// constant shader type.
+						// mrdoob/three.js#15469
+						console.warn( 'ColladaExporter: Texture maps not supported with MeshBasicMaterial.' );
+
+					}
+
+				}
+
+				var emissive = m.emissive ? m.emissive : new Color( 0, 0, 0 );
+				var diffuse = m.color ? m.color : new Color( 0, 0, 0 );
+				var specular = m.specular ? m.specular : new Color( 1, 1, 1 );
+				var shininess = m.shininess || 0;
+				var reflectivity = m.reflectivity || 0;
+
+				// Do not export and alpha map for the reasons mentioned in issue (#13792)
+				// in THREE.js alpha maps are black and white, but collada expects the alpha
+				// channel to specify the transparency
+				var transparencyNode = '';
+				if ( m.transparent === true ) {
+
+					transparencyNode +=
+						`<transparent>` +
+						(
+							m.map ?
+								`<texture texture="diffuse-sampler"></texture>` :
+								'<float>1</float>'
+						) +
+						'</transparent>';
+
+					if ( m.opacity < 1 ) {
+
+						transparencyNode += `<transparency><float>${ m.opacity }</float></transparency>`;
+
+					}
+
+				}
+
+				var techniqueNode = `<technique sid="common"><${ type }>` +
+
+					'<emission>' +
+
+					(
+						m.emissiveMap ?
+							'<texture texture="emissive-sampler" texcoord="TEXCOORD" />' :
+							`<color sid="emission">${ emissive.r } ${ emissive.g } ${ emissive.b } 1</color>`
+					) +
+
+					'</emission>' +
+
+					(
+						type !== 'constant' ?
+						'<diffuse>' +
+
+						(
+							m.map ?
+								'<texture texture="diffuse-sampler" texcoord="TEXCOORD" />' :
+								`<color sid="diffuse">${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color>`
+						) +
+						'</diffuse>'
+						: ''
+					) +
+
+					(
+						type === 'phong' ?
+						`<specular><color sid="specular">${ specular.r } ${ specular.g } ${ specular.b } 1</color></specular>` +
+
+						'<shininess>' +
+
+						(
+							m.specularMap ?
+								'<texture texture="specular-sampler" texcoord="TEXCOORD" />' :
+								`<float sid="shininess">${ shininess }</float>`
+						) +
+
+						'</shininess>'
+						: ''
+					) +
+
+					`<reflective><color>${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color></reflective>` +
+
+					`<reflectivity><float>${ reflectivity }</float></reflectivity>` +
+
+					transparencyNode +
+
+					`</${ type }></technique>`;
+
+				var effectnode =
+					`<effect id="${ matid }-effect">` +
+					'<profile_COMMON>' +
+
+					(
+						m.map ?
+							'<newparam sid="diffuse-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.map ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="diffuse-sampler"><sampler2D><source>diffuse-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					(
+						m.specularMap ?
+							'<newparam sid="specular-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.specularMap ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="specular-sampler"><sampler2D><source>specular-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					(
+						m.emissiveMap ?
+							'<newparam sid="emissive-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.emissiveMap ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="emissive-sampler"><sampler2D><source>emissive-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					techniqueNode +
+
+					(
+						m.side === DoubleSide ?
+							`<extra><technique><double_sided sid="double_sided" type="int">1</double_sided></technique></extra>` :
+							''
+					) +
+
+					'</profile_COMMON>' +
+
+					'</effect>';
+
+				libraryMaterials.push( `<material id="${ matid }" name="${ m.name }"><instance_effect url="#${ matid }-effect" /></material>` );
+				libraryEffects.push( effectnode );
+				materialMap.set( m, matid );
+
+			}
+
+			return matid;
+
+		}
+
+		// Recursively process the object into a scene
+		function processObject( o ) {
+
+			var node = `<node name="${ o.name }">`;
+
+			node += getTransform( o );
+
+			if ( o instanceof Mesh && o.geometry != null ) {
+
+				// function returns the id associated with the mesh and a "BufferGeometry" version
+				// of the geometry in case it's not a geometry.
+				var geomInfo = processGeometry( o.geometry );
+				var meshid = geomInfo.meshid;
+				var geometry = geomInfo.bufferGeometry;
+
+				// ids of the materials to bind to the geometry
+				var matids = null;
+				var matidsArray = [];
+
+				// get a list of materials to bind to the sub groups of the geometry.
+				// If the amount of subgroups is greater than the materials, than reuse
+				// the materials.
+				var mat = o.material || new MeshBasicMaterial();
+				var materials = Array.isArray( mat ) ? mat : [ mat ];
+				if ( geometry.groups.length > materials.length ) {
+					matidsArray = new Array( geometry.groups.length );
+				} else {
+					matidsArray = new Array( materials.length )
+				}
+				matids = matidsArray.fill()
+					.map( ( v, i ) => processMaterial( materials[ i % materials.length ] ) );
+
+				node +=
+					`<instance_geometry url="#${ meshid }">` +
+
+					(
+						matids != null ?
+							'<bind_material><technique_common>' +
+							matids.map( ( id, i ) =>
+
+								`<instance_material symbol="MESH_MATERIAL_${ i }" target="#${ id }" >` +
+
+								'<bind_vertex_input semantic="TEXCOORD" input_semantic="TEXCOORD" input_set="0" />' +
+
+								'</instance_material>'
+							).join( '' ) +
+							'</technique_common></bind_material>' :
+							''
+					) +
+
+					'</instance_geometry>';
+
+			}
+
+			o.children.forEach( c => node += processObject( c ) );
+
+			node += '</node>';
+
+			return node;
+
+		}
+
+		var geometryInfo = new WeakMap();
+		var materialMap = new WeakMap();
+		var imageMap = new WeakMap();
+		var textures = [];
+
+		var libraryImages = [];
+		var libraryGeometries = [];
+		var libraryEffects = [];
+		var libraryMaterials = [];
+		var libraryVisualScenes = processObject( object );
+
+		var specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/';
+		var dae =
+			'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' +
+			`<COLLADA xmlns="${ specLink }" version="${ version }">` +
+			'<asset>' +
+			(
+				'<contributor>' +
+				'<authoring_tool>THREE.js Collada Exporter</authoring_tool>' +
+				( options.author !== null ? `<author>${ options.author }</author>` : '' ) +
+				'</contributor>' +
+				`<created>${ ( new Date() ).toISOString() }</created>` +
+				`<modified>${ ( new Date() ).toISOString() }</modified>` +
+				'<up_axis>Y_UP</up_axis>'
+			) +
+			'</asset>';
+
+		dae += `<library_images>${ libraryImages.join( '' ) }</library_images>`;
+
+		dae += `<library_effects>${ libraryEffects.join( '' ) }</library_effects>`;
+
+		dae += `<library_materials>${ libraryMaterials.join( '' ) }</library_materials>`;
+
+		dae += `<library_geometries>${ libraryGeometries.join( '' ) }</library_geometries>`;
+
+		dae += `<library_visual_scenes><visual_scene id="Scene" name="scene">${ libraryVisualScenes }</visual_scene></library_visual_scenes>`;
+
+		dae += '<scene><instance_visual_scene url="#Scene"/></scene>';
+
+		dae += '</COLLADA>';
+
+		var res = {
+			data: format( dae ),
+			textures
+		};
+
+		if ( typeof onDone === 'function' ) {
+
+			requestAnimationFrame( () => onDone( res ) );
+
+		}
+
+		return res;
+
+	}
+
+};
+
+export { ColladaExporter };

+ 7 - 0
examples/jsm/exporters/GLTFExporter.d.ts

@@ -0,0 +1,7 @@
+import { Object3D } from '../../../src/Three';
+
+export class GLTFExporter {
+  constructor();
+
+  parse(input: Object3D, onCompleted: (gltf: object) => void, options: object): null;
+}

Some files were not shown because too many files changed in this diff