├── .gitignore ├── README.md ├── doc ├── blender_params.jpg ├── blender_params.psd └── moz_text_workflow.png ├── index.html ├── io_scene_gltf2_moz_text.zip ├── io_scene_gltf2_moz_text └── __init__.py ├── test ├── GLTFLoader.module.js ├── blendfiles │ ├── align.blend │ ├── basic.blend │ ├── letterspacing.blend │ ├── lineheight.blend │ ├── lorem ipsun.blend │ ├── materials.blend │ ├── overflow.blend │ ├── sizes.blend │ └── types.blend ├── bundle.js ├── bundle.js.map ├── fonts │ ├── Arial.json │ ├── Arial.ttf │ ├── Comic Sans MS Bold.ttf │ ├── Georgia.ttf │ ├── Impact.json │ └── Impact.ttf ├── index.html ├── index.js ├── models │ ├── align.gltf │ ├── basic.gltf │ ├── letterspacing.gltf │ ├── lineheight.gltf │ ├── lorem ipsun.gltf │ ├── materials.gltf │ ├── overflow.gltf │ ├── sizes.gltf │ └── types.gltf ├── package.json └── webpack.config.js └── threejs ├── GLTFLoader.module.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MOZ_text: Text extension for glTF 2 | 3 | This is an extension for glTF to support text objects. 4 | 5 | The aim is not to save the mesh but only metadata about the content, font and text properties, so it is in the final application the decision of how to interpret and render it. 6 | 7 | Currently only [threejs](https://github.com/mrdoob/three.js) is implemeted, temporarily modifying the [GLTFLoader](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/loaders/GLTFLoader.js) while the [support for plugins](https://github.com/mrdoob/three.js/pull/18484) is not officially merged. 8 | 9 | The ultimate goal would be to have a perfect match between Blender (or other 3d package) and the render engine, but given the multiple differences of how text is handled in all the steps, this is far from being perfect right now. Instead of that, this project aims to a very practical approach: to have a useful tool that we can use today for simple use cases. 10 | 11 | For comparisons between Blender and threejs scenes, try the [test scene](https://mixedreality.mozilla.org/MOZ_text) and compare it with the [blender scenes](https://github.com/feiss/MOZ_text/tree/master/test/blendfiles) 12 | 13 | ## Workflow and usage 14 | 15 | This is meant to be used as follows: 16 | 1. **Install Add-on** 17 | 18 | In Blender, install the [io_scene_gltf2_moz_text](https://github.com/feiss/MOZ_text/blob/master/io_scene_gltf2_moz_text.zip) add-on (`Edit > Preferences > Add-ons > "Install..."`), and turn on the add-on checkbox (see diagram below, 1). 19 | 20 | 2. **Add texts** 21 | 22 | Add and edit all the texts object in your scene (`shift+a > "Text"`). There is a MOZ_text panel at the bottom of the text properties (see diagram below, 2). 23 | 24 | 3. **Export** 25 | 26 | Export the scene (or selected objects) to glTF (`File > Export > glTF 2.0`), and find the extension general options at the end of the glTF options (see diagram below, 3). 27 | 28 | 4. **Add font files** 29 | 30 | Add the neccesary font files in your project folders. Use the same relative path as the one specified in the previous step. 31 | 32 | 5. **Load glTF** 33 | 34 | Use the custom GLTFLoader provided to load the glTF in threejs. 35 | 36 | ![workflow diagram](/doc/moz_text_workflow.png) 37 | 38 | 39 | ## Extension parameters 40 | 41 | Inside the glTF, the extension is added to a node as "MOZ_text", with these parameters: 42 | 43 | ```json 44 | "extensions" : { 45 | "MOZ_text" : { 46 | "index" : 1, 47 | "type" : "sdf | texture | geometry", 48 | "color" : [1.0, 0.98, 1.0, 1.0], 49 | "alignX" : "left", 50 | "alignY" : "top", 51 | "value" : "Lorem\nIpsun", 52 | "fontName" : "Impact", 53 | "fontFile" : "fonts/Impact.ttf", 54 | "size" : 2, 55 | "maxWidth" : 0, 56 | "overflow" : "NONE", 57 | "letterSpacing" : 0, 58 | "lineSpacing" : 0.56, 59 | "dimensions" : [9.7, 2.5], 60 | "extrude" : 0.63, 61 | "extrudeResolution" : 2, 62 | "bevel" : 0.039, 63 | "bevelOffset" : -0.019, 64 | "bevelResolution" : 1 65 | } 66 | } 67 | ``` 68 | 69 | ## How to run examples 70 | 71 | If you want to run https://mixedreality.mozilla.org/MOZ_text locally: 72 | 73 | 1. clone repo 74 | 2. `cd MOZ_text/test` 75 | 3. `npm install` 76 | 4. `npm run start` 77 | 5. open `localhost:8080` 78 | 79 | ## Blender parameters support 80 | 81 | In Blender, the MOZ_text panel under the text properties allows to specify how the text should be rendered in the application. Current possible values are `SDF`, `Texture` or `Geometry`. 82 | 83 | Currently only these text parameters are exported: 84 | 85 | ![blender params](/doc/blender_params.jpg) 86 | 87 | Depending on `type`, some parameters are ignored. For example, bevel and extrude parameters are only used if text `type` is `geometry`. 88 | 89 | -------------------------------------------------------------------------------- /doc/blender_params.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/doc/blender_params.jpg -------------------------------------------------------------------------------- /doc/blender_params.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/doc/blender_params.psd -------------------------------------------------------------------------------- /doc/moz_text_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/doc/moz_text_workflow.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /io_scene_gltf2_moz_text.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/io_scene_gltf2_moz_text.zip -------------------------------------------------------------------------------- /io_scene_gltf2_moz_text/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from os import path 3 | from bpy.app.handlers import persistent 4 | 5 | bl_info = { 6 | "name": "MOZ_text glTF Extension", 7 | "category": "Import-Export", 8 | "version": (1, 0, 0), 9 | "blender": (2, 80, 0), 10 | 'location': 'File > Export > glTF 2.0', 11 | 'description': 'Custom glTF extension to support text nodes', 12 | 'tracker_url': "https://", # Replace with your issue tracker 13 | 'isDraft': False, 14 | 'developer': "Diego F. Goberna", # Replace this 15 | 'url': 'https://', # Replace this 16 | } 17 | 18 | # glTF extensions are named following a convention with known prefixes. 19 | # See: https://github.com/KhronosGroup/glTF/tree/master/extensions#about-gltf-extensions 20 | # also: https://github.com/KhronosGroup/glTF/blob/master/extensions/Prefixes.md 21 | glTF_extension_name = "MOZ_text" 22 | 23 | # Support for an extension is "required" if a typical glTF viewer cannot be expected 24 | # to load a given model without understanding the contents of the extension. 25 | # For example, a compression scheme or new image format (with no fallback included) 26 | # would be "required", but physics metadata or app-specific settings could be optional. 27 | extension_is_required = False 28 | 29 | class MOZTextProperties(bpy.types.PropertyGroup): 30 | font_path: bpy.props.StringProperty( 31 | name='Font path', 32 | description='Relative folder in your application to the font files.', 33 | default='fonts/' 34 | ) 35 | 36 | def register(): 37 | bpy.utils.register_class(MOZTextProperties) 38 | bpy.types.Scene.MOZTextProperties = bpy.props.PointerProperty(type=MOZTextProperties) 39 | bpy.utils.register_class(AdditionalTextPropertiesPanel) 40 | 41 | bpy.app.handlers.frame_change_pre.append(afterExport) 42 | 43 | def register_panel(): 44 | # Register the panel on demand, we need to be sure to only register it once 45 | # This is necessary because the panel is a child of the extensions panel, 46 | # which may not be registered when we try to register this extension 47 | try: 48 | bpy.utils.register_class(GLTF_PT_UserExtensionPanel) 49 | except Exception: 50 | pass 51 | 52 | # If the glTF exporter is disabled, we need to unregister the extension panel 53 | # Just return a function to the exporter so it can unregister the panel 54 | return unregister_panel 55 | 56 | 57 | def unregister_panel(): 58 | # Since panel is registered on demand, it is possible it is not registered 59 | try: 60 | bpy.utils.unregister_class(GLTF_PT_UserExtensionPanel) 61 | bpy.utils.unregister_class(AdditionalTextPropertiesPanel) 62 | except Exception: 63 | pass 64 | 65 | 66 | def unregister(): 67 | unregister_panel() 68 | bpy.utils.unregister_class(MOZTextProperties) 69 | del bpy.types.Scene.MOZTextProperties 70 | 71 | bpy.app.handlers.frame_change_pre.remove(afterExport) 72 | 73 | class GLTF_PT_UserExtensionPanel(bpy.types.Panel): 74 | 75 | bl_space_type = 'FILE_BROWSER' 76 | bl_region_type = 'TOOL_PROPS' 77 | bl_label = "Enabled" 78 | bl_parent_id = "GLTF_PT_export_user_extensions" 79 | bl_options = {'DEFAULT_CLOSED'} 80 | 81 | @classmethod 82 | def poll(cls, context): 83 | sfile = context.space_data 84 | operator = sfile.active_operator 85 | return operator.bl_idname == "EXPORT_SCENE_OT_gltf" 86 | 87 | def draw_header(self, context): 88 | props = bpy.context.scene.MOZTextProperties 89 | self.layout.prop(props, 'enabled') 90 | 91 | def draw(self, context): 92 | layout = self.layout 93 | layout.use_property_split = True 94 | layout.use_property_decorate = False # No animation. 95 | 96 | props = bpy.context.scene.MOZTextProperties 97 | layout.active = props.enabled 98 | 99 | props = bpy.context.scene.MOZTextProperties 100 | layout.prop(props, 'font_path') 101 | 102 | 103 | class glTF2ExportUserExtension: 104 | def __init__(self): 105 | # We need to wait until we create the gltf2UserExtension to import the gltf2 modules 106 | # Otherwise, it may fail because the gltf2 may not be loaded yet 107 | from io_scene_gltf2.io.com.gltf2_io_extensions import Extension 108 | self.Extension = Extension 109 | self.properties = bpy.context.scene.MOZTextProperties 110 | self.text_index = 0 111 | 112 | def gather_node_hook(self, gltf2_object, blender_object, export_settings): 113 | if not blender_object.type == 'FONT': 114 | return 115 | 116 | o = blender_object 117 | 118 | o.undoExportActions = True 119 | 120 | fontPath = bpy.context.scene.MOZTextProperties.font_path.strip() 121 | if fontPath and fontPath[-1] != '/': fontPath += '/' 122 | 123 | ext_data = dict() 124 | ext_data['index'] = self.text_index 125 | ext_data['type'] = o.text_type.lower() 126 | ext_data['color'] = (o.text_color[0], o.text_color[1], o.text_color[2], o.text_color[3]) 127 | ext_data['alignX'] = o.data.align_x.lower() 128 | ext_data['alignY'] = o.data.align_y.lower() 129 | ext_data['value'] = o.data.body 130 | ext_data['fontName'] = o.data.font.name # postscript name 131 | ext_data['fontFile'] = fontPath + o.data.font.filepath.split(path.sep).pop() 132 | ext_data['size'] = o.data.size 133 | ext_data['maxWidth'] = o.data.text_boxes[0].width 134 | ext_data['overflow'] = o.data.overflow 135 | ext_data['letterSpacing'] = o.data.space_character - 1 136 | ext_data['lineSpacing'] = o.data.space_line 137 | ext_data['dimensions'] = (o.dimensions.x, o.dimensions.y) 138 | # extrude/bevel 139 | if ext_data['type'] == 'geometry': 140 | ext_data['extrude'] = o.data.extrude 141 | ext_data['extrudeResolution'] = o.data.resolution_u 142 | ext_data['bevel'] = o.data.bevel_depth 143 | ext_data['bevelOffset'] = o.data.offset 144 | ext_data['bevelResolution'] = o.data.bevel_resolution 145 | 146 | # renderEngine = 'CYCLES' if bpy.context.scene.render.engine == 'CYCLES' else 'EEVEE' 147 | # material = o.active_material 148 | # if material.use_nodes: 149 | # color = material.node_tree.get_output_node(renderEngine).inputs[0].links[0].from_node.inputs[0].default_value 150 | # else: 151 | # color = material.diffuse_color 152 | # ext_data['color'] = [color[0], color[1], color[2]] 153 | 154 | self.text_index += 1 155 | 156 | if self.properties.enabled: 157 | if gltf2_object.extensions is None: 158 | gltf2_object.extensions = {} 159 | gltf2_object.extensions[glTF_extension_name] = self.Extension( 160 | name = glTF_extension_name, 161 | extension = ext_data, 162 | required = extension_is_required 163 | ) 164 | 165 | text_type = ( 166 | ('TEXTURE', "Texture", "Text rendered on a texture"), 167 | ('GEOMETRY', "Geometry", "Text rendered with a polygon mesh"), 168 | ('SDF', "SDF", "Text rendered with a SDF shader") 169 | ) 170 | 171 | bpy.types.Object.text_type = bpy.props.EnumProperty( 172 | name = "Text Type", 173 | description = "How this text should be rendered in realtime", 174 | items = text_type, 175 | default = 'SDF', 176 | ) 177 | 178 | bpy.types.Object.text_color = bpy.props.FloatVectorProperty( 179 | name = "Text Color", 180 | description = "Text color", 181 | default = (1.0, 1.0, 1.0, 1.0), 182 | size = 4, 183 | min = 0, 184 | max = 1, 185 | subtype = 'COLOR' 186 | ) 187 | 188 | 189 | class AdditionalTextPropertiesPanel(bpy.types.Panel): 190 | """Creates a Panel in the data properties window""" 191 | bl_label = "MOZ_text extension" 192 | bl_idname = "OBJECT_PT_moztextpanel" 193 | bl_space_type = 'PROPERTIES' 194 | bl_region_type = 'WINDOW' 195 | bl_context = "data" 196 | 197 | def draw(self, context): 198 | layout = self.layout 199 | 200 | obj = context.object 201 | if type(obj.data).__name__ != 'TextCurve': return 202 | 203 | row = layout.row() 204 | row.prop(obj, "text_type") 205 | row = layout.row() 206 | row.prop(obj, "text_color") 207 | 208 | 209 | bpy.types.Object.undoExportActions = bpy.props.BoolProperty( 210 | name = "undoExportActions", 211 | description = "whether undoing modifications made during glTF export is needed", 212 | default = False 213 | ) 214 | 215 | @persistent 216 | def afterExport(scene): 217 | for obj in bpy.data.objects: 218 | if not obj.type == 'FONT' or not obj.undoExportActions: continue 219 | obj.undoExportActions = False 220 | 221 | -------------------------------------------------------------------------------- /test/GLTFLoader.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Rich Tibbett / https://github.com/richtr 3 | * @author mrdoob / http://mrdoob.com/ 4 | * @author Tony Parisi / http://www.tonyparisi.com/ 5 | * @author Takahiro / https://github.com/takahirox 6 | * @author Don McCurdy / https://www.donmccurdy.com 7 | */ 8 | 9 | 10 | import { 11 | AnimationClip, 12 | Bone, 13 | Box3, 14 | BufferAttribute, 15 | BufferGeometry, 16 | ClampToEdgeWrapping, 17 | Color, 18 | DirectionalLight, 19 | DoubleSide, 20 | FileLoader, 21 | FrontSide, 22 | Group, 23 | InterleavedBuffer, 24 | InterleavedBufferAttribute, 25 | Interpolant, 26 | InterpolateDiscrete, 27 | InterpolateLinear, 28 | Line, 29 | LineBasicMaterial, 30 | LineLoop, 31 | LineSegments, 32 | LinearFilter, 33 | LinearMipmapLinearFilter, 34 | LinearMipmapNearestFilter, 35 | Loader, 36 | LoaderUtils, 37 | Material, 38 | MathUtils, 39 | Matrix4, 40 | Mesh, 41 | MeshBasicMaterial, 42 | MeshStandardMaterial, 43 | MirroredRepeatWrapping, 44 | NearestFilter, 45 | NearestMipmapLinearFilter, 46 | NearestMipmapNearestFilter, 47 | NumberKeyframeTrack, 48 | Object3D, 49 | OrthographicCamera, 50 | PerspectiveCamera, 51 | PointLight, 52 | Points, 53 | PointsMaterial, 54 | PropertyBinding, 55 | QuaternionKeyframeTrack, 56 | RGBAFormat, 57 | RGBFormat, 58 | RepeatWrapping, 59 | Skeleton, 60 | SkinnedMesh, 61 | Sphere, 62 | SpotLight, 63 | TangentSpaceNormalMap, 64 | TextureLoader, 65 | TriangleFanDrawMode, 66 | TriangleStripDrawMode, 67 | Vector2, 68 | Vector3, 69 | VectorKeyframeTrack, 70 | sRGBEncoding, 71 | FontLoader, 72 | TextBufferGeometry, 73 | PlaneBufferGeometry, 74 | CanvasTexture, 75 | MeshLambertMaterial 76 | } from "three"; 77 | 78 | import {TextMesh} from 'troika-3d-text/dist/textmesh-standalone.esm.js'; 79 | 80 | 81 | var GLTFLoader = ( function () { 82 | 83 | function GLTFLoader( manager ) { 84 | 85 | Loader.call( this, manager ); 86 | 87 | this.dracoLoader = null; 88 | this.ddsLoader = null; 89 | 90 | } 91 | 92 | GLTFLoader.prototype = Object.assign( Object.create( Loader.prototype ), { 93 | 94 | constructor: GLTFLoader, 95 | 96 | load: function ( url, onLoad, onProgress, onError ) { 97 | 98 | var scope = this; 99 | 100 | var resourcePath; 101 | 102 | if ( this.resourcePath !== '' ) { 103 | 104 | resourcePath = this.resourcePath; 105 | 106 | } else if ( this.path !== '' ) { 107 | 108 | resourcePath = this.path; 109 | 110 | } else { 111 | 112 | resourcePath = LoaderUtils.extractUrlBase( url ); 113 | 114 | } 115 | 116 | // Tells the LoadingManager to track an extra item, which resolves after 117 | // the model is fully loaded. This means the count of items loaded will 118 | // be incorrect, but ensures manager.onLoad() does not fire early. 119 | scope.manager.itemStart( url ); 120 | 121 | var _onError = function ( e ) { 122 | 123 | if ( onError ) { 124 | 125 | onError( e ); 126 | 127 | } else { 128 | 129 | console.error( e ); 130 | 131 | } 132 | 133 | scope.manager.itemError( url ); 134 | scope.manager.itemEnd( url ); 135 | 136 | }; 137 | 138 | var loader = new FileLoader( scope.manager ); 139 | 140 | loader.setPath( this.path ); 141 | loader.setResponseType( 'arraybuffer' ); 142 | 143 | if ( scope.crossOrigin === 'use-credentials' ) { 144 | 145 | loader.setWithCredentials( true ); 146 | 147 | } 148 | 149 | loader.load( url, function ( data ) { 150 | 151 | try { 152 | 153 | scope.parse( data, resourcePath, function ( gltf ) { 154 | 155 | onLoad( gltf ); 156 | 157 | scope.manager.itemEnd( url ); 158 | 159 | }, _onError ); 160 | 161 | } catch ( e ) { 162 | 163 | _onError( e ); 164 | 165 | } 166 | 167 | }, onProgress, _onError ); 168 | 169 | }, 170 | 171 | setDRACOLoader: function ( dracoLoader ) { 172 | 173 | this.dracoLoader = dracoLoader; 174 | return this; 175 | 176 | }, 177 | 178 | setDDSLoader: function ( ddsLoader ) { 179 | 180 | this.ddsLoader = ddsLoader; 181 | return this; 182 | 183 | }, 184 | 185 | parse: function ( data, path, onLoad, onError ) { 186 | 187 | var content; 188 | var extensions = {}; 189 | 190 | if ( typeof data === 'string' ) { 191 | 192 | content = data; 193 | 194 | } else { 195 | 196 | var magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); 197 | 198 | if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { 199 | 200 | try { 201 | 202 | extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); 203 | 204 | } catch ( error ) { 205 | 206 | if ( onError ) onError( error ); 207 | return; 208 | 209 | } 210 | 211 | content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content; 212 | 213 | } else { 214 | 215 | content = LoaderUtils.decodeText( new Uint8Array( data ) ); 216 | 217 | } 218 | 219 | } 220 | 221 | var json = JSON.parse( content ); 222 | 223 | if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { 224 | 225 | if ( onError ) onError( new Error( 'GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); 226 | return; 227 | 228 | } 229 | 230 | if ( json.extensionsUsed ) { 231 | 232 | for ( var i = 0; i < json.extensionsUsed.length; ++ i ) { 233 | 234 | var extensionName = json.extensionsUsed[ i ]; 235 | var extensionsRequired = json.extensionsRequired || []; 236 | 237 | switch ( extensionName ) { 238 | 239 | case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: 240 | extensions[ extensionName ] = new GLTFLightsExtension( json ); 241 | break; 242 | 243 | case EXTENSIONS.KHR_MATERIALS_CLEARCOAT: 244 | extensions[ extensionName ] = new GLTFMaterialsClearcoatExtension(); 245 | break; 246 | 247 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 248 | extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); 249 | break; 250 | 251 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 252 | extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension(); 253 | break; 254 | 255 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 256 | extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); 257 | break; 258 | 259 | case EXTENSIONS.MSFT_TEXTURE_DDS: 260 | extensions[ extensionName ] = new GLTFTextureDDSExtension( this.ddsLoader ); 261 | break; 262 | 263 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 264 | extensions[ extensionName ] = new GLTFTextureTransformExtension(); 265 | break; 266 | 267 | case EXTENSIONS.KHR_MESH_QUANTIZATION: 268 | extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); 269 | break; 270 | 271 | case EXTENSIONS.MOZ_TEXT: 272 | extensions[ extensionName ] = new GLTFTextExtension( json ); 273 | break; 274 | 275 | default: 276 | 277 | if ( extensionsRequired.indexOf( extensionName ) >= 0 ) { 278 | 279 | console.warn( 'GLTFLoader: Unknown extension "' + extensionName + '".' ); 280 | 281 | } 282 | 283 | } 284 | 285 | } 286 | 287 | } 288 | 289 | var parser = new GLTFParser( json, extensions, { 290 | 291 | path: path || this.resourcePath || '', 292 | crossOrigin: this.crossOrigin, 293 | manager: this.manager 294 | 295 | } ); 296 | 297 | parser.parse( onLoad, onError ); 298 | 299 | } 300 | 301 | } ); 302 | 303 | /* GLTFREGISTRY */ 304 | 305 | function GLTFRegistry() { 306 | 307 | var objects = {}; 308 | 309 | return { 310 | 311 | get: function ( key ) { 312 | 313 | return objects[ key ]; 314 | 315 | }, 316 | 317 | add: function ( key, object ) { 318 | 319 | objects[ key ] = object; 320 | 321 | }, 322 | 323 | remove: function ( key ) { 324 | 325 | delete objects[ key ]; 326 | 327 | }, 328 | 329 | removeAll: function () { 330 | 331 | objects = {}; 332 | 333 | } 334 | 335 | }; 336 | 337 | } 338 | 339 | /*********************************/ 340 | /********** EXTENSIONS ***********/ 341 | /*********************************/ 342 | 343 | var EXTENSIONS = { 344 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 345 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 346 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 347 | KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', 348 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 349 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 350 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 351 | KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', 352 | MSFT_TEXTURE_DDS: 'MSFT_texture_dds', 353 | MOZ_TEXT: 'MOZ_text' 354 | }; 355 | 356 | /** 357 | * DDS Texture Extension 358 | * 359 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds 360 | * 361 | */ 362 | function GLTFTextureDDSExtension( ddsLoader ) { 363 | 364 | if ( ! ddsLoader ) { 365 | 366 | throw new Error( 'GLTFLoader: Attempting to load .dds texture without importing DDSLoader' ); 367 | 368 | } 369 | 370 | this.name = EXTENSIONS.MSFT_TEXTURE_DDS; 371 | this.ddsLoader = ddsLoader; 372 | 373 | } 374 | 375 | /** 376 | * Punctual Lights Extension 377 | * 378 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 379 | */ 380 | function GLTFLightsExtension( json ) { 381 | 382 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; 383 | 384 | var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] ) || {}; 385 | this.lightDefs = extension.lights || []; 386 | 387 | } 388 | 389 | GLTFLightsExtension.prototype.loadLight = function ( lightIndex ) { 390 | 391 | var lightDef = this.lightDefs[ lightIndex ]; 392 | var lightNode; 393 | 394 | var color = new Color( 0xffffff ); 395 | if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); 396 | 397 | var range = lightDef.range !== undefined ? lightDef.range : 0; 398 | 399 | switch ( lightDef.type ) { 400 | 401 | case 'directional': 402 | lightNode = new DirectionalLight( color ); 403 | lightNode.target.position.set( 0, 0, - 1 ); 404 | lightNode.add( lightNode.target ); 405 | break; 406 | 407 | case 'point': 408 | lightNode = new PointLight( color ); 409 | lightNode.distance = range; 410 | break; 411 | 412 | case 'spot': 413 | lightNode = new SpotLight( color ); 414 | lightNode.distance = range; 415 | // Handle spotlight properties. 416 | lightDef.spot = lightDef.spot || {}; 417 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 418 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 419 | lightNode.angle = lightDef.spot.outerConeAngle; 420 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 421 | lightNode.target.position.set( 0, 0, - 1 ); 422 | lightNode.add( lightNode.target ); 423 | break; 424 | 425 | default: 426 | throw new Error( 'GLTFLoader: Unexpected light type, "' + lightDef.type + '".' ); 427 | 428 | } 429 | 430 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 431 | // here, because node-level parsing will only override position if explicitly specified. 432 | lightNode.position.set( 0, 0, 0 ); 433 | 434 | lightNode.decay = 2; 435 | 436 | if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; 437 | 438 | lightNode.name = lightDef.name || ( 'light_' + lightIndex ); 439 | 440 | return Promise.resolve( lightNode ); 441 | 442 | }; 443 | 444 | /** 445 | * Unlit Materials Extension 446 | * 447 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit 448 | */ 449 | function GLTFMaterialsUnlitExtension() { 450 | 451 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 452 | 453 | } 454 | 455 | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { 456 | 457 | return MeshBasicMaterial; 458 | 459 | }; 460 | 461 | GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) { 462 | 463 | var pending = []; 464 | 465 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 466 | materialParams.opacity = 1.0; 467 | 468 | var metallicRoughness = materialDef.pbrMetallicRoughness; 469 | 470 | if ( metallicRoughness ) { 471 | 472 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 473 | 474 | var array = metallicRoughness.baseColorFactor; 475 | 476 | materialParams.color.fromArray( array ); 477 | materialParams.opacity = array[ 3 ]; 478 | 479 | } 480 | 481 | if ( metallicRoughness.baseColorTexture !== undefined ) { 482 | 483 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 484 | 485 | } 486 | 487 | } 488 | 489 | return Promise.all( pending ); 490 | 491 | }; 492 | 493 | /** 494 | * Clearcoat Materials Extension 495 | * 496 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat 497 | */ 498 | function GLTFMaterialsClearcoatExtension() { 499 | 500 | this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; 501 | 502 | } 503 | 504 | GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function () { 505 | 506 | return MeshPhysicalMaterial; 507 | 508 | }; 509 | 510 | GLTFMaterialsClearcoatExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) { 511 | 512 | var pending = []; 513 | 514 | var extension = materialDef.extensions[ this.name ]; 515 | 516 | if ( extension.clearcoatFactor !== undefined ) { 517 | 518 | materialParams.clearcoat = extension.clearcoatFactor; 519 | 520 | } 521 | 522 | if ( extension.clearcoatTexture !== undefined ) { 523 | 524 | pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); 525 | 526 | } 527 | 528 | if ( extension.clearcoatRoughnessFactor !== undefined ) { 529 | 530 | materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; 531 | 532 | } 533 | 534 | if ( extension.clearcoatRoughnessTexture !== undefined ) { 535 | 536 | pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); 537 | 538 | } 539 | 540 | if ( extension.clearcoatNormalTexture !== undefined ) { 541 | 542 | pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); 543 | 544 | if ( extension.clearcoatNormalTexture.scale !== undefined ) { 545 | 546 | var scale = extension.clearcoatNormalTexture.scale; 547 | 548 | materialParams.clearcoatNormalScale = new Vector2( scale, scale ); 549 | 550 | } 551 | 552 | } 553 | 554 | return Promise.all( pending ); 555 | 556 | }; 557 | 558 | /* BINARY EXTENSION */ 559 | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 560 | var BINARY_EXTENSION_HEADER_LENGTH = 12; 561 | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; 562 | 563 | function GLTFBinaryExtension( data ) { 564 | 565 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 566 | this.content = null; 567 | this.body = null; 568 | 569 | var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); 570 | 571 | this.header = { 572 | magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), 573 | version: headerView.getUint32( 4, true ), 574 | length: headerView.getUint32( 8, true ) 575 | }; 576 | 577 | if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { 578 | 579 | throw new Error( 'GLTFLoader: Unsupported glTF-Binary header.' ); 580 | 581 | } else if ( this.header.version < 2.0 ) { 582 | 583 | throw new Error( 'GLTFLoader: Legacy binary file detected.' ); 584 | 585 | } 586 | 587 | var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); 588 | var chunkIndex = 0; 589 | 590 | while ( chunkIndex < chunkView.byteLength ) { 591 | 592 | var chunkLength = chunkView.getUint32( chunkIndex, true ); 593 | chunkIndex += 4; 594 | 595 | var chunkType = chunkView.getUint32( chunkIndex, true ); 596 | chunkIndex += 4; 597 | 598 | if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { 599 | 600 | var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); 601 | this.content = LoaderUtils.decodeText( contentArray ); 602 | 603 | } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { 604 | 605 | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 606 | this.body = data.slice( byteOffset, byteOffset + chunkLength ); 607 | 608 | } 609 | 610 | // Clients must ignore chunks with unknown types. 611 | 612 | chunkIndex += chunkLength; 613 | 614 | } 615 | 616 | if ( this.content === null ) { 617 | 618 | throw new Error( 'GLTFLoader: JSON content not found.' ); 619 | 620 | } 621 | 622 | } 623 | 624 | /** 625 | * DRACO Mesh Compression Extension 626 | * 627 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression 628 | */ 629 | function GLTFDracoMeshCompressionExtension( json, dracoLoader ) { 630 | 631 | if ( ! dracoLoader ) { 632 | 633 | throw new Error( 'GLTFLoader: No DRACOLoader instance provided.' ); 634 | 635 | } 636 | 637 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 638 | this.json = json; 639 | this.dracoLoader = dracoLoader; 640 | this.dracoLoader.preload(); 641 | 642 | } 643 | 644 | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { 645 | 646 | var json = this.json; 647 | var dracoLoader = this.dracoLoader; 648 | var bufferViewIndex = primitive.extensions[ this.name ].bufferView; 649 | var gltfAttributeMap = primitive.extensions[ this.name ].attributes; 650 | var threeAttributeMap = {}; 651 | var attributeNormalizedMap = {}; 652 | var attributeTypeMap = {}; 653 | 654 | for ( var attributeName in gltfAttributeMap ) { 655 | 656 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 657 | 658 | threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; 659 | 660 | } 661 | 662 | for ( attributeName in primitive.attributes ) { 663 | 664 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 665 | 666 | if ( gltfAttributeMap[ attributeName ] !== undefined ) { 667 | 668 | var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; 669 | var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 670 | 671 | attributeTypeMap[ threeAttributeName ] = componentType; 672 | attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; 673 | 674 | } 675 | 676 | } 677 | 678 | return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { 679 | 680 | return new Promise( function ( resolve ) { 681 | 682 | dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { 683 | 684 | for ( var attributeName in geometry.attributes ) { 685 | 686 | var attribute = geometry.attributes[ attributeName ]; 687 | var normalized = attributeNormalizedMap[ attributeName ]; 688 | 689 | if ( normalized !== undefined ) attribute.normalized = normalized; 690 | 691 | } 692 | 693 | resolve( geometry ); 694 | 695 | }, threeAttributeMap, attributeTypeMap ); 696 | 697 | } ); 698 | 699 | } ); 700 | 701 | }; 702 | 703 | /** 704 | * Texture Transform Extension 705 | * 706 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform 707 | */ 708 | function GLTFTextureTransformExtension() { 709 | 710 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 711 | 712 | } 713 | 714 | GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) { 715 | 716 | texture = texture.clone(); 717 | 718 | if ( transform.offset !== undefined ) { 719 | 720 | texture.offset.fromArray( transform.offset ); 721 | 722 | } 723 | 724 | if ( transform.rotation !== undefined ) { 725 | 726 | texture.rotation = transform.rotation; 727 | 728 | } 729 | 730 | if ( transform.scale !== undefined ) { 731 | 732 | texture.repeat.fromArray( transform.scale ); 733 | 734 | } 735 | 736 | if ( transform.texCoord !== undefined ) { 737 | 738 | console.warn( 'GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); 739 | 740 | } 741 | 742 | texture.needsUpdate = true; 743 | 744 | return texture; 745 | 746 | }; 747 | 748 | /** 749 | * Specular-Glossiness Extension 750 | * 751 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 752 | */ 753 | 754 | /** 755 | * A sub class of StandardMaterial with some of the functionality 756 | * changed via the `onBeforeCompile` callback 757 | * @pailhead 758 | */ 759 | 760 | function GLTFMeshStandardSGMaterial( params ) { 761 | 762 | MeshStandardMaterial.call( this ); 763 | 764 | this.isGLTFSpecularGlossinessMaterial = true; 765 | 766 | //various chunks that need replacing 767 | var specularMapParsFragmentChunk = [ 768 | '#ifdef USE_SPECULARMAP', 769 | ' uniform sampler2D specularMap;', 770 | '#endif' 771 | ].join( '\n' ); 772 | 773 | var glossinessMapParsFragmentChunk = [ 774 | '#ifdef USE_GLOSSINESSMAP', 775 | ' uniform sampler2D glossinessMap;', 776 | '#endif' 777 | ].join( '\n' ); 778 | 779 | var specularMapFragmentChunk = [ 780 | 'vec3 specularFactor = specular;', 781 | '#ifdef USE_SPECULARMAP', 782 | ' vec4 texelSpecular = texture2D( specularMap, vUv );', 783 | ' texelSpecular = sRGBToLinear( texelSpecular );', 784 | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', 785 | ' specularFactor *= texelSpecular.rgb;', 786 | '#endif' 787 | ].join( '\n' ); 788 | 789 | var glossinessMapFragmentChunk = [ 790 | 'float glossinessFactor = glossiness;', 791 | '#ifdef USE_GLOSSINESSMAP', 792 | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', 793 | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', 794 | ' glossinessFactor *= texelGlossiness.a;', 795 | '#endif' 796 | ].join( '\n' ); 797 | 798 | var lightPhysicalFragmentChunk = [ 799 | 'PhysicalMaterial material;', 800 | 'material.diffuseColor = diffuseColor.rgb;', 801 | 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', 802 | 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', 803 | 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 );// 0.0525 corresponds to the base mip of a 256 cubemap.', 804 | 'material.specularRoughness += geometryRoughness;', 805 | 'material.specularRoughness = min( material.specularRoughness, 1.0 );', 806 | 'material.specularColor = specularFactor.rgb;', 807 | ].join( '\n' ); 808 | 809 | var uniforms = { 810 | specular: { value: new Color().setHex( 0xffffff ) }, 811 | glossiness: { value: 1 }, 812 | specularMap: { value: null }, 813 | glossinessMap: { value: null } 814 | }; 815 | 816 | this._extraUniforms = uniforms; 817 | 818 | // please see #14031 or #13198 for an alternate approach 819 | this.onBeforeCompile = function ( shader ) { 820 | 821 | for ( var uniformName in uniforms ) { 822 | 823 | shader.uniforms[ uniformName ] = uniforms[ uniformName ]; 824 | 825 | } 826 | 827 | shader.fragmentShader = shader.fragmentShader.replace( 'uniform float roughness;', 'uniform vec3 specular;' ); 828 | shader.fragmentShader = shader.fragmentShader.replace( 'uniform float metalness;', 'uniform float glossiness;' ); 829 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', specularMapParsFragmentChunk ); 830 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', glossinessMapParsFragmentChunk ); 831 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', specularMapFragmentChunk ); 832 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', glossinessMapFragmentChunk ); 833 | shader.fragmentShader = shader.fragmentShader.replace( '#include ', lightPhysicalFragmentChunk ); 834 | 835 | }; 836 | 837 | /*eslint-disable*/ 838 | Object.defineProperties( 839 | this, 840 | { 841 | specular: { 842 | get: function () { return uniforms.specular.value; }, 843 | set: function ( v ) { uniforms.specular.value = v; } 844 | }, 845 | specularMap: { 846 | get: function () { return uniforms.specularMap.value; }, 847 | set: function ( v ) { uniforms.specularMap.value = v; } 848 | }, 849 | glossiness: { 850 | get: function () { return uniforms.glossiness.value; }, 851 | set: function ( v ) { uniforms.glossiness.value = v; } 852 | }, 853 | glossinessMap: { 854 | get: function () { return uniforms.glossinessMap.value; }, 855 | set: function ( v ) { 856 | 857 | uniforms.glossinessMap.value = v; 858 | //how about something like this - @pailhead 859 | if ( v ) { 860 | 861 | this.defines.USE_GLOSSINESSMAP = ''; 862 | // set USE_ROUGHNESSMAP to enable vUv 863 | this.defines.USE_ROUGHNESSMAP = ''; 864 | 865 | } else { 866 | 867 | delete this.defines.USE_ROUGHNESSMAP; 868 | delete this.defines.USE_GLOSSINESSMAP; 869 | 870 | } 871 | 872 | } 873 | } 874 | } 875 | ); 876 | 877 | /*eslint-enable*/ 878 | delete this.metalness; 879 | delete this.roughness; 880 | delete this.metalnessMap; 881 | delete this.roughnessMap; 882 | 883 | this.setValues( params ); 884 | 885 | } 886 | 887 | GLTFMeshStandardSGMaterial.prototype = Object.create( MeshStandardMaterial.prototype ); 888 | GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial; 889 | 890 | GLTFMeshStandardSGMaterial.prototype.copy = function ( source ) { 891 | 892 | MeshStandardMaterial.prototype.copy.call( this, source ); 893 | this.specularMap = source.specularMap; 894 | this.specular.copy( source.specular ); 895 | this.glossinessMap = source.glossinessMap; 896 | this.glossiness = source.glossiness; 897 | delete this.metalness; 898 | delete this.roughness; 899 | delete this.metalnessMap; 900 | delete this.roughnessMap; 901 | return this; 902 | 903 | }; 904 | 905 | function GLTFMaterialsPbrSpecularGlossinessExtension() { 906 | 907 | return { 908 | 909 | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, 910 | 911 | specularGlossinessParams: [ 912 | 'color', 913 | 'map', 914 | 'lightMap', 915 | 'lightMapIntensity', 916 | 'aoMap', 917 | 'aoMapIntensity', 918 | 'emissive', 919 | 'emissiveIntensity', 920 | 'emissiveMap', 921 | 'bumpMap', 922 | 'bumpScale', 923 | 'normalMap', 924 | 'normalMapType', 925 | 'displacementMap', 926 | 'displacementScale', 927 | 'displacementBias', 928 | 'specularMap', 929 | 'specular', 930 | 'glossinessMap', 931 | 'glossiness', 932 | 'alphaMap', 933 | 'envMap', 934 | 'envMapIntensity', 935 | 'refractionRatio', 936 | ], 937 | 938 | getMaterialType: function () { 939 | 940 | return GLTFMeshStandardSGMaterial; 941 | 942 | }, 943 | 944 | extendParams: function ( materialParams, materialDef, parser ) { 945 | 946 | var pbrSpecularGlossiness = materialDef.extensions[ this.name ]; 947 | 948 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 949 | materialParams.opacity = 1.0; 950 | 951 | var pending = []; 952 | 953 | if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { 954 | 955 | var array = pbrSpecularGlossiness.diffuseFactor; 956 | 957 | materialParams.color.fromArray( array ); 958 | materialParams.opacity = array[ 3 ]; 959 | 960 | } 961 | 962 | if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { 963 | 964 | pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); 965 | 966 | } 967 | 968 | materialParams.emissive = new Color( 0.0, 0.0, 0.0 ); 969 | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 970 | materialParams.specular = new Color( 1.0, 1.0, 1.0 ); 971 | 972 | if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { 973 | 974 | materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); 975 | 976 | } 977 | 978 | if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { 979 | 980 | var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 981 | pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); 982 | pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); 983 | 984 | } 985 | 986 | return Promise.all( pending ); 987 | 988 | }, 989 | 990 | createMaterial: function ( materialParams ) { 991 | 992 | var material = new GLTFMeshStandardSGMaterial( materialParams ); 993 | material.fog = true; 994 | 995 | material.color = materialParams.color; 996 | 997 | material.map = materialParams.map === undefined ? null : materialParams.map; 998 | 999 | material.lightMap = null; 1000 | material.lightMapIntensity = 1.0; 1001 | 1002 | material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; 1003 | material.aoMapIntensity = 1.0; 1004 | 1005 | material.emissive = materialParams.emissive; 1006 | material.emissiveIntensity = 1.0; 1007 | material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; 1008 | 1009 | material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; 1010 | material.bumpScale = 1; 1011 | 1012 | material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; 1013 | material.normalMapType = TangentSpaceNormalMap; 1014 | 1015 | if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; 1016 | 1017 | material.displacementMap = null; 1018 | material.displacementScale = 1; 1019 | material.displacementBias = 0; 1020 | 1021 | material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; 1022 | material.specular = materialParams.specular; 1023 | 1024 | material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; 1025 | material.glossiness = materialParams.glossiness; 1026 | 1027 | material.alphaMap = null; 1028 | 1029 | material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; 1030 | material.envMapIntensity = 1.0; 1031 | 1032 | material.refractionRatio = 0.98; 1033 | 1034 | return material; 1035 | 1036 | }, 1037 | 1038 | }; 1039 | 1040 | } 1041 | 1042 | /** 1043 | * Mesh Quantization Extension 1044 | * 1045 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization 1046 | */ 1047 | function GLTFMeshQuantizationExtension() { 1048 | 1049 | this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; 1050 | 1051 | } 1052 | 1053 | /** 1054 | * Text Extension 1055 | * 1056 | * Specification: ??? 1057 | */ 1058 | function GLTFTextExtension(json) { 1059 | 1060 | this.name = EXTENSIONS.MOZ_TEXT; 1061 | 1062 | var extension = ( json.extensions && json.extensions[ EXTENSIONS.MOZ_TEXT ] ) || {}; 1063 | this.json = json; 1064 | } 1065 | 1066 | GLTFTextExtension.prototype.createText = async function ( index ) { 1067 | // find node 1068 | for (var i = 0; i < this.json.nodes.length; i++){ 1069 | if (this.json.nodes[i].extensions 1070 | && this.json.nodes[i].extensions[ EXTENSIONS.MOZ_TEXT ] 1071 | && this.json.nodes[i].extensions[ EXTENSIONS.MOZ_TEXT ]['index'] === index ) { 1072 | 1073 | const jsonNode = this.json.nodes[i]; 1074 | const textConfig = jsonNode.extensions[ EXTENSIONS.MOZ_TEXT ]; 1075 | let node; 1076 | switch(textConfig.type) { 1077 | case 'sdf': node = this.createSDFTextNode(textConfig, jsonNode); break; 1078 | case 'texture': node = this.createTextureTextNode(textConfig, jsonNode); break; 1079 | case 'geometry': node = await this.createGeometryTextNode(textConfig, jsonNode); break; 1080 | } 1081 | 1082 | return Promise.resolve( node ); 1083 | } 1084 | } 1085 | } 1086 | 1087 | const SDF_TEXT_MAGIC_SIZE_FACTOR = 1.4; 1088 | const SDF_TEXT_MAGIC_LINESPACING_FACTOR = 1.3; 1089 | const SDF_TEXT_MAGIC_LETTERSPACING_FACTOR = 0.66; 1090 | 1091 | GLTFTextExtension.prototype.createSDFTextNode = function ( text, jsonNode ) { 1092 | var anchor = [ 1093 | text.alignX == 'right' ? 1 : (text.alignX == 'center' ? 0.5 : 0), 1094 | text.alignY == 'bottom' ? 1 : (text.alignY == 'center' ? 0.5 : 0), 1095 | ]; 1096 | if (text.alignY == 'top_baseline') { 1097 | const numLines = text.value.split('\n').length; 1098 | anchor[1] = 1 / numLines; 1099 | } 1100 | const node = new TextMesh(); 1101 | node.orientation = '+x-z'; 1102 | node.text = text.value; 1103 | if (text.fontFile !== '') { 1104 | node.font = text.fontFile; 1105 | } 1106 | node.fontSize = text.size / SDF_TEXT_MAGIC_SIZE_FACTOR; 1107 | node.textAlign = text.alignX; 1108 | node.lineHeight = text.lineSpacing * SDF_TEXT_MAGIC_LINESPACING_FACTOR; 1109 | node.letterSpacing = text.letterSpacing * SDF_TEXT_MAGIC_LETTERSPACING_FACTOR; 1110 | node.anchor = anchor; 1111 | node.maxWidth = text.maxWidth === 0 ? Infinity : text.maxWidth; 1112 | node.whiteSpace = node.maxWidth === Infinity ? 'nowrap' : 'normal'; 1113 | node.material = new MeshBasicMaterial({ 1114 | color: new Color(text.color[0], text.color[1], text.color[2]), 1115 | transparent: text.color[3] < 1, 1116 | opacity: text.color[3] 1117 | }); 1118 | node.sync(); 1119 | return node; 1120 | } 1121 | 1122 | const TEXTURE_TEXT_MAGIC_SIZE_FACTOR = 0.62; 1123 | const TEXTURE_TEXT_SAFE_MARGIN = 2; // to avoid texture clipping 1124 | 1125 | GLTFTextExtension.prototype.createTextureTextNode = function ( text, jsonNode ) { 1126 | const scalex = jsonNode.scale && jsonNode.scale[0] || 1; 1127 | const scaley = jsonNode.scale && jsonNode.scale[1] || 1; 1128 | const width = text.dimensions[0] / scalex; 1129 | const height = text.dimensions[1] / scaley; 1130 | const sizeRatio = height / width; 1131 | const geometry = new PlaneBufferGeometry(width, height); 1132 | const canvas = document.createElement('canvas'); 1133 | 1134 | /* // show canvas for debug 1135 | canvas.style.position = "absolute"; 1136 | canvas.style.left = '0'; 1137 | canvas.style.top = '0'; 1138 | canvas.style.background = '#000'; 1139 | canvas.zIndex = 9999999; 1140 | document.body.appendChild(canvas) 1141 | */ 1142 | 1143 | canvas.width = 1024; 1144 | canvas.height = 1024 * sizeRatio; 1145 | const c = canvas.getContext('2d'); 1146 | c.font = Math.floor(canvas.width / width * text.size * TEXTURE_TEXT_MAGIC_SIZE_FACTOR) + "px " + text.fontName; 1147 | const metrics = c.measureText(text.value); 1148 | c.textAlign = text.alignX; 1149 | const descent = metrics.actualBoundingBoxDescent; 1150 | const ascent = metrics.actualBoundingBoxAscent; 1151 | c.fillStyle = '#FFF'; 1152 | var tx = TEXTURE_TEXT_SAFE_MARGIN; 1153 | switch(text.alignX) { 1154 | case 'center': tx = canvas.width / 2; break; 1155 | case 'right': tx = canvas.width - TEXTURE_TEXT_SAFE_MARGIN; break; 1156 | } 1157 | 1158 | const lines = text.value.split('\n'); 1159 | for (var i = 0; i < lines.length; i++) { 1160 | c.fillText( 1161 | lines[i], 1162 | tx, 1163 | ascent + i * ascent * text.lineSpacing * 2 + TEXTURE_TEXT_SAFE_MARGIN); 1164 | } 1165 | 1166 | var ty = 0; 1167 | switch (text.alignY){ 1168 | case 'bottom_baseline': ty = height / 2 - height * descent / canvas.height; break; 1169 | case 'bottom': ty = height / 2; break; 1170 | case 'top': ty = -height / 2; break; 1171 | case 'top_baseline': ty = -height / 2 + ascent / canvas.height * height; break; 1172 | } 1173 | 1174 | geometry.translate(0, ty, 0) 1175 | geometry.rotateX(-Math.PI / 2); 1176 | 1177 | const texture = new CanvasTexture(canvas); 1178 | const material = new MeshBasicMaterial({ 1179 | color: new Color(text.color[0], text.color[1], text.color[2]), 1180 | map: texture, 1181 | transparent: true, 1182 | opacity: text.color[3] 1183 | }); 1184 | const node = new Mesh(geometry, material); 1185 | return node; 1186 | } 1187 | 1188 | const GEOMETRY_TEXT_MAGIC_SIZE_FACTOR = 1.88; 1189 | const GEOMETRY_TEXT_MAGIC_BB_YMIN = 0; 1190 | const GEOMETRY_TEXT_MAGIC_BB_YMAX = 2000; 1191 | const GEOMETRY_TEXT_MAGIC_BEVEL_THICKNESS = 0.1; 1192 | 1193 | GLTFTextExtension.prototype.createGeometryTextNode = async function ( text, jsonNode ) { 1194 | return new Promise(resolve => { 1195 | 1196 | if (text.fontFile == '') { 1197 | console.warn("GLTFLoader: geometry text does not have a font defined (\"" + text.value + "\")"); 1198 | resolve(new Object3D()); 1199 | } 1200 | 1201 | new FontLoader().load( 1202 | text.fontFile.substring(0, text.fontFile.lastIndexOf('.')) + '.json', 1203 | 1204 | font => { 1205 | //magic boundingBox change to better match Blender's look 1206 | font.data.boundingBox.yMin = GEOMETRY_TEXT_MAGIC_BB_YMIN; 1207 | font.data.boundingBox.yMax = GEOMETRY_TEXT_MAGIC_BB_YMAX * text.lineSpacing; 1208 | const geometry = new TextBufferGeometry( 1209 | text.value, 1210 | { 1211 | font: font, 1212 | size: text.size / GEOMETRY_TEXT_MAGIC_SIZE_FACTOR, 1213 | height: text.extrude * 2, 1214 | curveSegments: text.extrudeResolution, 1215 | bevelEnabled: text.bevel > 0, 1216 | bevelThickness: GEOMETRY_TEXT_MAGIC_BEVEL_THICKNESS, 1217 | bevelSize: text.bevel, 1218 | bevelOffset: text.bevelOffset, 1219 | bevelSegments: text.bevelResolution 1220 | } 1221 | ); 1222 | 1223 | geometry.computeBoundingBox(); 1224 | const size = geometry.boundingBox.getSize(); 1225 | 1226 | var dx; 1227 | switch(text.alignX) { 1228 | case 'left': dx = 0; break; 1229 | case 'right': dx = -size.x; break; 1230 | default: dx = -size.x / 2; break; 1231 | } 1232 | 1233 | var dy; 1234 | switch(text.alignY) { 1235 | case 'top': case 'top_baseline': dy = -size.y / 2; break; 1236 | case 'bottom': case 'bottom_baseline': dy = size.y; break; 1237 | default: dy = 0; break; 1238 | } 1239 | 1240 | geometry.translate(dx, dy, -text.extrude); 1241 | geometry.rotateX(-Math.PI / 2); 1242 | 1243 | const material = new MeshLambertMaterial({ 1244 | color: new Color(text.color[0], text.color[1], text.color[2]), 1245 | transparent: text.color[3] < 1, 1246 | opacity: text.color[3], 1247 | }); 1248 | 1249 | resolve(new Mesh(geometry, material)); 1250 | }); 1251 | }); 1252 | 1253 | } 1254 | 1255 | 1256 | /*********************************/ 1257 | /********** INTERPOLATION ********/ 1258 | /*********************************/ 1259 | 1260 | // Spline Interpolation 1261 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 1262 | function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { 1263 | 1264 | Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); 1265 | 1266 | } 1267 | 1268 | GLTFCubicSplineInterpolant.prototype = Object.create( Interpolant.prototype ); 1269 | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; 1270 | 1271 | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) { 1272 | 1273 | // Copies a sample value to the result buffer. See description of glTF 1274 | // CUBICSPLINE values layout in interpolate_() function below. 1275 | 1276 | var result = this.resultBuffer, 1277 | values = this.sampleValues, 1278 | valueSize = this.valueSize, 1279 | offset = index * valueSize * 3 + valueSize; 1280 | 1281 | for ( var i = 0; i !== valueSize; i ++ ) { 1282 | 1283 | result[ i ] = values[ offset + i ]; 1284 | 1285 | } 1286 | 1287 | return result; 1288 | 1289 | }; 1290 | 1291 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1292 | 1293 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1294 | 1295 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { 1296 | 1297 | var result = this.resultBuffer; 1298 | var values = this.sampleValues; 1299 | var stride = this.valueSize; 1300 | 1301 | var stride2 = stride * 2; 1302 | var stride3 = stride * 3; 1303 | 1304 | var td = t1 - t0; 1305 | 1306 | var p = ( t - t0 ) / td; 1307 | var pp = p * p; 1308 | var ppp = pp * p; 1309 | 1310 | var offset1 = i1 * stride3; 1311 | var offset0 = offset1 - stride3; 1312 | 1313 | var s2 = - 2 * ppp + 3 * pp; 1314 | var s3 = ppp - pp; 1315 | var s0 = 1 - s2; 1316 | var s1 = s3 - pp + p; 1317 | 1318 | // Layout of keyframe output values for CUBICSPLINE animations: 1319 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 1320 | for ( var i = 0; i !== stride; i ++ ) { 1321 | 1322 | var p0 = values[ offset0 + i + stride ]; // splineVertex_k 1323 | var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) 1324 | var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 1325 | var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) 1326 | 1327 | result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 1328 | 1329 | } 1330 | 1331 | return result; 1332 | 1333 | }; 1334 | 1335 | /*********************************/ 1336 | /********** INTERNALS ************/ 1337 | /*********************************/ 1338 | 1339 | /* CONSTANTS */ 1340 | 1341 | var WEBGL_CONSTANTS = { 1342 | FLOAT: 5126, 1343 | //FLOAT_MAT2: 35674, 1344 | FLOAT_MAT3: 35675, 1345 | FLOAT_MAT4: 35676, 1346 | FLOAT_VEC2: 35664, 1347 | FLOAT_VEC3: 35665, 1348 | FLOAT_VEC4: 35666, 1349 | LINEAR: 9729, 1350 | REPEAT: 10497, 1351 | SAMPLER_2D: 35678, 1352 | POINTS: 0, 1353 | LINES: 1, 1354 | LINE_LOOP: 2, 1355 | LINE_STRIP: 3, 1356 | TRIANGLES: 4, 1357 | TRIANGLE_STRIP: 5, 1358 | TRIANGLE_FAN: 6, 1359 | UNSIGNED_BYTE: 5121, 1360 | UNSIGNED_SHORT: 5123 1361 | }; 1362 | 1363 | var WEBGL_COMPONENT_TYPES = { 1364 | 5120: Int8Array, 1365 | 5121: Uint8Array, 1366 | 5122: Int16Array, 1367 | 5123: Uint16Array, 1368 | 5125: Uint32Array, 1369 | 5126: Float32Array 1370 | }; 1371 | 1372 | var WEBGL_FILTERS = { 1373 | 9728: NearestFilter, 1374 | 9729: LinearFilter, 1375 | 9984: NearestMipmapNearestFilter, 1376 | 9985: LinearMipmapNearestFilter, 1377 | 9986: NearestMipmapLinearFilter, 1378 | 9987: LinearMipmapLinearFilter 1379 | }; 1380 | 1381 | var WEBGL_WRAPPINGS = { 1382 | 33071: ClampToEdgeWrapping, 1383 | 33648: MirroredRepeatWrapping, 1384 | 10497: RepeatWrapping 1385 | }; 1386 | 1387 | var WEBGL_TYPE_SIZES = { 1388 | 'SCALAR': 1, 1389 | 'VEC2': 2, 1390 | 'VEC3': 3, 1391 | 'VEC4': 4, 1392 | 'MAT2': 4, 1393 | 'MAT3': 9, 1394 | 'MAT4': 16 1395 | }; 1396 | 1397 | var ATTRIBUTES = { 1398 | POSITION: 'position', 1399 | NORMAL: 'normal', 1400 | TANGENT: 'tangent', 1401 | TEXCOORD_0: 'uv', 1402 | TEXCOORD_1: 'uv2', 1403 | COLOR_0: 'color', 1404 | WEIGHTS_0: 'skinWeight', 1405 | JOINTS_0: 'skinIndex', 1406 | }; 1407 | 1408 | var PATH_PROPERTIES = { 1409 | scale: 'scale', 1410 | translation: 'position', 1411 | rotation: 'quaternion', 1412 | weights: 'morphTargetInfluences' 1413 | }; 1414 | 1415 | var INTERPOLATION = { 1416 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1417 | // keyframe track will be initialized with a default interpolation type, then modified. 1418 | LINEAR: InterpolateLinear, 1419 | STEP: InterpolateDiscrete 1420 | }; 1421 | 1422 | var ALPHA_MODES = { 1423 | OPAQUE: 'OPAQUE', 1424 | MASK: 'MASK', 1425 | BLEND: 'BLEND' 1426 | }; 1427 | 1428 | var MIME_TYPE_FORMATS = { 1429 | 'image/png': RGBAFormat, 1430 | 'image/jpeg': RGBFormat 1431 | }; 1432 | 1433 | /* UTILITY FUNCTIONS */ 1434 | 1435 | function resolveURL( url, path ) { 1436 | 1437 | // Invalid URL 1438 | if ( typeof url !== 'string' || url === '' ) return ''; 1439 | 1440 | // Host Relative URL 1441 | if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) { 1442 | 1443 | path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' ); 1444 | 1445 | } 1446 | 1447 | // Absolute URL http://,https://,// 1448 | if ( /^(https?:)?\/\//i.test( url ) ) return url; 1449 | 1450 | // Data URI 1451 | if ( /^data:.*,.*$/i.test( url ) ) return url; 1452 | 1453 | // Blob URL 1454 | if ( /^blob:.*$/i.test( url ) ) return url; 1455 | 1456 | // Relative URL 1457 | return path + url; 1458 | 1459 | } 1460 | 1461 | /** 1462 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1463 | */ 1464 | function createDefaultMaterial( cache ) { 1465 | 1466 | if ( cache[ 'DefaultMaterial' ] === undefined ) { 1467 | 1468 | cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { 1469 | color: 0xFFFFFF, 1470 | emissive: 0x000000, 1471 | metalness: 1, 1472 | roughness: 1, 1473 | transparent: false, 1474 | depthTest: true, 1475 | side: FrontSide 1476 | } ); 1477 | 1478 | } 1479 | 1480 | return cache[ 'DefaultMaterial' ]; 1481 | 1482 | } 1483 | 1484 | function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { 1485 | 1486 | // Add unknown glTF extensions to an object's userData. 1487 | 1488 | for ( var name in objectDef.extensions ) { 1489 | 1490 | if ( knownExtensions[ name ] === undefined ) { 1491 | 1492 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 1493 | object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; 1494 | 1495 | } 1496 | 1497 | } 1498 | 1499 | } 1500 | 1501 | /** 1502 | * @param {Object3D|Material|BufferGeometry} object 1503 | * @param {GLTF.definition} gltfDef 1504 | */ 1505 | function assignExtrasToUserData( object, gltfDef ) { 1506 | 1507 | if ( gltfDef.extras !== undefined ) { 1508 | 1509 | if ( typeof gltfDef.extras === 'object' ) { 1510 | 1511 | Object.assign( object.userData, gltfDef.extras ); 1512 | 1513 | } else { 1514 | 1515 | console.warn( 'GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); 1516 | 1517 | } 1518 | 1519 | } 1520 | 1521 | } 1522 | 1523 | /** 1524 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1525 | * 1526 | * @param {BufferGeometry} geometry 1527 | * @param {Array} targets 1528 | * @param {GLTFParser} parser 1529 | * @return {Promise} 1530 | */ 1531 | function addMorphTargets( geometry, targets, parser ) { 1532 | 1533 | var hasMorphPosition = false; 1534 | var hasMorphNormal = false; 1535 | 1536 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1537 | 1538 | var target = targets[ i ]; 1539 | 1540 | if ( target.POSITION !== undefined ) hasMorphPosition = true; 1541 | if ( target.NORMAL !== undefined ) hasMorphNormal = true; 1542 | 1543 | if ( hasMorphPosition && hasMorphNormal ) break; 1544 | 1545 | } 1546 | 1547 | if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); 1548 | 1549 | var pendingPositionAccessors = []; 1550 | var pendingNormalAccessors = []; 1551 | 1552 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1553 | 1554 | var target = targets[ i ]; 1555 | 1556 | if ( hasMorphPosition ) { 1557 | 1558 | var pendingAccessor = target.POSITION !== undefined 1559 | ? parser.getDependency( 'accessor', target.POSITION ) 1560 | : geometry.attributes.position; 1561 | 1562 | pendingPositionAccessors.push( pendingAccessor ); 1563 | 1564 | } 1565 | 1566 | if ( hasMorphNormal ) { 1567 | 1568 | var pendingAccessor = target.NORMAL !== undefined 1569 | ? parser.getDependency( 'accessor', target.NORMAL ) 1570 | : geometry.attributes.normal; 1571 | 1572 | pendingNormalAccessors.push( pendingAccessor ); 1573 | 1574 | } 1575 | 1576 | } 1577 | 1578 | return Promise.all( [ 1579 | Promise.all( pendingPositionAccessors ), 1580 | Promise.all( pendingNormalAccessors ) 1581 | ] ).then( function ( accessors ) { 1582 | 1583 | var morphPositions = accessors[ 0 ]; 1584 | var morphNormals = accessors[ 1 ]; 1585 | 1586 | if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; 1587 | if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; 1588 | geometry.morphTargetsRelative = true; 1589 | 1590 | return geometry; 1591 | 1592 | } ); 1593 | 1594 | } 1595 | 1596 | /** 1597 | * @param {Mesh} mesh 1598 | * @param {GLTF.Mesh} meshDef 1599 | */ 1600 | function updateMorphTargets( mesh, meshDef ) { 1601 | 1602 | mesh.updateMorphTargets(); 1603 | 1604 | if ( meshDef.weights !== undefined ) { 1605 | 1606 | for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) { 1607 | 1608 | mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; 1609 | 1610 | } 1611 | 1612 | } 1613 | 1614 | // .extras has user-defined data, so check that .extras.targetNames is an array. 1615 | if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { 1616 | 1617 | var targetNames = meshDef.extras.targetNames; 1618 | 1619 | if ( mesh.morphTargetInfluences.length === targetNames.length ) { 1620 | 1621 | mesh.morphTargetDictionary = {}; 1622 | 1623 | for ( var i = 0, il = targetNames.length; i < il; i ++ ) { 1624 | 1625 | mesh.morphTargetDictionary[ targetNames[ i ] ] = i; 1626 | 1627 | } 1628 | 1629 | } else { 1630 | 1631 | console.warn( 'GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); 1632 | 1633 | } 1634 | 1635 | } 1636 | 1637 | } 1638 | 1639 | function createPrimitiveKey( primitiveDef ) { 1640 | 1641 | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; 1642 | var geometryKey; 1643 | 1644 | if ( dracoExtension ) { 1645 | 1646 | geometryKey = 'draco:' + dracoExtension.bufferView 1647 | + ':' + dracoExtension.indices 1648 | + ':' + createAttributesKey( dracoExtension.attributes ); 1649 | 1650 | } else { 1651 | 1652 | geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; 1653 | 1654 | } 1655 | 1656 | return geometryKey; 1657 | 1658 | } 1659 | 1660 | function createAttributesKey( attributes ) { 1661 | 1662 | var attributesKey = ''; 1663 | 1664 | var keys = Object.keys( attributes ).sort(); 1665 | 1666 | for ( var i = 0, il = keys.length; i < il; i ++ ) { 1667 | 1668 | attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; 1669 | 1670 | } 1671 | 1672 | return attributesKey; 1673 | 1674 | } 1675 | 1676 | /* GLTF PARSER */ 1677 | 1678 | function GLTFParser( json, extensions, options ) { 1679 | 1680 | this.json = json || {}; 1681 | this.extensions = extensions || {}; 1682 | this.options = options || {}; 1683 | 1684 | // loader object cache 1685 | this.cache = new GLTFRegistry(); 1686 | 1687 | // BufferGeometry caching 1688 | this.primitiveCache = {}; 1689 | 1690 | this.textureLoader = new TextureLoader( this.options.manager ); 1691 | this.textureLoader.setCrossOrigin( this.options.crossOrigin ); 1692 | 1693 | this.fileLoader = new FileLoader( this.options.manager ); 1694 | this.fileLoader.setResponseType( 'arraybuffer' ); 1695 | 1696 | if ( this.options.crossOrigin === 'use-credentials' ) { 1697 | 1698 | this.fileLoader.setWithCredentials( true ); 1699 | 1700 | } 1701 | 1702 | } 1703 | 1704 | GLTFParser.prototype.parse = function ( onLoad, onError ) { 1705 | 1706 | var parser = this; 1707 | var json = this.json; 1708 | var extensions = this.extensions; 1709 | 1710 | // Clear the loader cache 1711 | this.cache.removeAll(); 1712 | 1713 | // Mark the special nodes/meshes in json for efficient parse 1714 | this.markDefs(); 1715 | 1716 | Promise.all( [ 1717 | 1718 | this.getDependencies( 'scene' ), 1719 | this.getDependencies( 'animation' ), 1720 | this.getDependencies( 'camera' ), 1721 | 1722 | ] ).then( function ( dependencies ) { 1723 | 1724 | var result = { 1725 | scene: dependencies[ 0 ][ json.scene || 0 ], 1726 | scenes: dependencies[ 0 ], 1727 | animations: dependencies[ 1 ], 1728 | cameras: dependencies[ 2 ], 1729 | asset: json.asset, 1730 | parser: parser, 1731 | userData: {} 1732 | }; 1733 | 1734 | addUnknownExtensionsToUserData( extensions, result, json ); 1735 | 1736 | assignExtrasToUserData( result, json ); 1737 | 1738 | onLoad( result ); 1739 | 1740 | } ).catch( onError ); 1741 | 1742 | }; 1743 | 1744 | /** 1745 | * Marks the special nodes/meshes in json for efficient parse. 1746 | */ 1747 | GLTFParser.prototype.markDefs = function () { 1748 | 1749 | var nodeDefs = this.json.nodes || []; 1750 | var skinDefs = this.json.skins || []; 1751 | var meshDefs = this.json.meshes || []; 1752 | 1753 | var meshReferences = {}; 1754 | var meshUses = {}; 1755 | 1756 | // Nothing in the node definition indicates whether it is a Bone or an 1757 | // Object3D. Use the skins' joint references to mark bones. 1758 | for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { 1759 | 1760 | var joints = skinDefs[ skinIndex ].joints; 1761 | 1762 | for ( var i = 0, il = joints.length; i < il; i ++ ) { 1763 | 1764 | nodeDefs[ joints[ i ] ].isBone = true; 1765 | 1766 | } 1767 | 1768 | } 1769 | 1770 | // Meshes can (and should) be reused by multiple nodes in a glTF asset. To 1771 | // avoid having more than one Mesh with the same name, count 1772 | // references and rename instances below. 1773 | // 1774 | // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 1775 | for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 1776 | 1777 | var nodeDef = nodeDefs[ nodeIndex ]; 1778 | 1779 | if ( nodeDef.mesh !== undefined ) { 1780 | 1781 | if ( meshReferences[ nodeDef.mesh ] === undefined ) { 1782 | 1783 | meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0; 1784 | 1785 | } 1786 | 1787 | meshReferences[ nodeDef.mesh ] ++; 1788 | 1789 | // Nothing in the mesh definition indicates whether it is 1790 | // a SkinnedMesh or Mesh. Use the node's mesh reference 1791 | // to mark SkinnedMesh if node has skin. 1792 | if ( nodeDef.skin !== undefined ) { 1793 | 1794 | meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; 1795 | 1796 | } 1797 | 1798 | } 1799 | 1800 | } 1801 | 1802 | this.json.meshReferences = meshReferences; 1803 | this.json.meshUses = meshUses; 1804 | 1805 | }; 1806 | 1807 | /** 1808 | * Requests the specified dependency asynchronously, with caching. 1809 | * @param {string} type 1810 | * @param {number} index 1811 | * @return {Promise} 1812 | */ 1813 | GLTFParser.prototype.getDependency = function ( type, index ) { 1814 | 1815 | var cacheKey = type + ':' + index; 1816 | var dependency = this.cache.get( cacheKey ); 1817 | 1818 | if ( ! dependency ) { 1819 | 1820 | switch ( type ) { 1821 | 1822 | case 'scene': 1823 | dependency = this.loadScene( index ); 1824 | break; 1825 | 1826 | case 'node': 1827 | dependency = this.loadNode( index ); 1828 | break; 1829 | 1830 | case 'mesh': 1831 | dependency = this.loadMesh( index ); 1832 | break; 1833 | 1834 | case 'accessor': 1835 | dependency = this.loadAccessor( index ); 1836 | break; 1837 | 1838 | case 'bufferView': 1839 | dependency = this.loadBufferView( index ); 1840 | break; 1841 | 1842 | case 'buffer': 1843 | dependency = this.loadBuffer( index ); 1844 | break; 1845 | 1846 | case 'material': 1847 | dependency = this.loadMaterial( index ); 1848 | break; 1849 | 1850 | case 'texture': 1851 | dependency = this.loadTexture( index ); 1852 | break; 1853 | 1854 | case 'skin': 1855 | dependency = this.loadSkin( index ); 1856 | break; 1857 | 1858 | case 'animation': 1859 | dependency = this.loadAnimation( index ); 1860 | break; 1861 | 1862 | case 'camera': 1863 | dependency = this.loadCamera( index ); 1864 | break; 1865 | 1866 | case 'light': 1867 | dependency = this.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].loadLight( index ); 1868 | break; 1869 | 1870 | case 'text': 1871 | dependency = this.extensions[ EXTENSIONS.MOZ_TEXT ].createText( index ); 1872 | break; 1873 | 1874 | default: 1875 | throw new Error( 'Unknown type: ' + type ); 1876 | 1877 | } 1878 | 1879 | this.cache.add( cacheKey, dependency ); 1880 | 1881 | } 1882 | 1883 | return dependency; 1884 | 1885 | }; 1886 | 1887 | /** 1888 | * Requests all dependencies of the specified type asynchronously, with caching. 1889 | * @param {string} type 1890 | * @return {Promise>} 1891 | */ 1892 | GLTFParser.prototype.getDependencies = function ( type ) { 1893 | 1894 | var dependencies = this.cache.get( type ); 1895 | 1896 | if ( ! dependencies ) { 1897 | 1898 | var parser = this; 1899 | var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; 1900 | 1901 | dependencies = Promise.all( defs.map( function ( def, index ) { 1902 | 1903 | return parser.getDependency( type, index ); 1904 | 1905 | } ) ); 1906 | 1907 | this.cache.add( type, dependencies ); 1908 | 1909 | } 1910 | 1911 | return dependencies; 1912 | 1913 | }; 1914 | 1915 | /** 1916 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1917 | * @param {number} bufferIndex 1918 | * @return {Promise} 1919 | */ 1920 | GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { 1921 | 1922 | var bufferDef = this.json.buffers[ bufferIndex ]; 1923 | var loader = this.fileLoader; 1924 | 1925 | if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { 1926 | 1927 | throw new Error( 'GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); 1928 | 1929 | } 1930 | 1931 | // If present, GLB container is required to be the first buffer. 1932 | if ( bufferDef.uri === undefined && bufferIndex === 0 ) { 1933 | 1934 | return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); 1935 | 1936 | } 1937 | 1938 | var options = this.options; 1939 | 1940 | return new Promise( function ( resolve, reject ) { 1941 | 1942 | loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { 1943 | 1944 | reject( new Error( 'GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); 1945 | 1946 | } ); 1947 | 1948 | } ); 1949 | 1950 | }; 1951 | 1952 | /** 1953 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1954 | * @param {number} bufferViewIndex 1955 | * @return {Promise} 1956 | */ 1957 | GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) { 1958 | 1959 | var bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; 1960 | 1961 | return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { 1962 | 1963 | var byteLength = bufferViewDef.byteLength || 0; 1964 | var byteOffset = bufferViewDef.byteOffset || 0; 1965 | return buffer.slice( byteOffset, byteOffset + byteLength ); 1966 | 1967 | } ); 1968 | 1969 | }; 1970 | 1971 | /** 1972 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 1973 | * @param {number} accessorIndex 1974 | * @return {Promise} 1975 | */ 1976 | GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { 1977 | 1978 | var parser = this; 1979 | var json = this.json; 1980 | 1981 | var accessorDef = this.json.accessors[ accessorIndex ]; 1982 | 1983 | if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { 1984 | 1985 | // Ignore empty accessors, which may be used to declare runtime 1986 | // information about attributes coming from another source (e.g. Draco 1987 | // compression extension). 1988 | return Promise.resolve( null ); 1989 | 1990 | } 1991 | 1992 | var pendingBufferViews = []; 1993 | 1994 | if ( accessorDef.bufferView !== undefined ) { 1995 | 1996 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); 1997 | 1998 | } else { 1999 | 2000 | pendingBufferViews.push( null ); 2001 | 2002 | } 2003 | 2004 | if ( accessorDef.sparse !== undefined ) { 2005 | 2006 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); 2007 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); 2008 | 2009 | } 2010 | 2011 | return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { 2012 | 2013 | var bufferView = bufferViews[ 0 ]; 2014 | 2015 | var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; 2016 | var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 2017 | 2018 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 2019 | var elementBytes = TypedArray.BYTES_PER_ELEMENT; 2020 | var itemBytes = elementBytes * itemSize; 2021 | var byteOffset = accessorDef.byteOffset || 0; 2022 | var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; 2023 | var normalized = accessorDef.normalized === true; 2024 | var array, bufferAttribute; 2025 | 2026 | // The buffer is not interleaved if the stride is the item size in bytes. 2027 | if ( byteStride && byteStride !== itemBytes ) { 2028 | 2029 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer 2030 | // This makes sure that IBA.count reflects accessor.count properly 2031 | var ibSlice = Math.floor( byteOffset / byteStride ); 2032 | var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; 2033 | var ib = parser.cache.get( ibCacheKey ); 2034 | 2035 | if ( ! ib ) { 2036 | 2037 | array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); 2038 | 2039 | // Integer parameters to IB/IBA are in array elements, not bytes. 2040 | ib = new InterleavedBuffer( array, byteStride / elementBytes ); 2041 | 2042 | parser.cache.add( ibCacheKey, ib ); 2043 | 2044 | } 2045 | 2046 | bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); 2047 | 2048 | } else { 2049 | 2050 | if ( bufferView === null ) { 2051 | 2052 | array = new TypedArray( accessorDef.count * itemSize ); 2053 | 2054 | } else { 2055 | 2056 | array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); 2057 | 2058 | } 2059 | 2060 | bufferAttribute = new BufferAttribute( array, itemSize, normalized ); 2061 | 2062 | } 2063 | 2064 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 2065 | if ( accessorDef.sparse !== undefined ) { 2066 | 2067 | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 2068 | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; 2069 | 2070 | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 2071 | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 2072 | 2073 | var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); 2074 | var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); 2075 | 2076 | if ( bufferView !== null ) { 2077 | 2078 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 2079 | bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); 2080 | 2081 | } 2082 | 2083 | for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { 2084 | 2085 | var index = sparseIndices[ i ]; 2086 | 2087 | bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); 2088 | if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); 2089 | if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); 2090 | if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); 2091 | if ( itemSize >= 5 ) throw new Error( 'GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); 2092 | 2093 | } 2094 | 2095 | } 2096 | 2097 | return bufferAttribute; 2098 | 2099 | } ); 2100 | 2101 | }; 2102 | 2103 | /** 2104 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 2105 | * @param {number} textureIndex 2106 | * @return {Promise} 2107 | */ 2108 | GLTFParser.prototype.loadTexture = function ( textureIndex ) { 2109 | 2110 | var parser = this; 2111 | var json = this.json; 2112 | var options = this.options; 2113 | var textureLoader = this.textureLoader; 2114 | 2115 | var URL = self.URL || self.webkitURL; 2116 | 2117 | var textureDef = json.textures[ textureIndex ]; 2118 | 2119 | var textureExtensions = textureDef.extensions || {}; 2120 | 2121 | var source; 2122 | 2123 | if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) { 2124 | 2125 | source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ]; 2126 | 2127 | } else { 2128 | 2129 | source = json.images[ textureDef.source ]; 2130 | 2131 | } 2132 | 2133 | var sourceURI = source.uri; 2134 | var isObjectURL = false; 2135 | 2136 | if ( source.bufferView !== undefined ) { 2137 | 2138 | // Load binary image data from bufferView, if provided. 2139 | 2140 | sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { 2141 | 2142 | isObjectURL = true; 2143 | var blob = new Blob( [ bufferView ], { type: source.mimeType } ); 2144 | sourceURI = URL.createObjectURL( blob ); 2145 | return sourceURI; 2146 | 2147 | } ); 2148 | 2149 | } 2150 | 2151 | return Promise.resolve( sourceURI ).then( function ( sourceURI ) { 2152 | 2153 | // Load Texture resource. 2154 | 2155 | var loader = options.manager.getHandler( sourceURI ); 2156 | 2157 | if ( ! loader ) { 2158 | 2159 | loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] 2160 | ? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader 2161 | : textureLoader; 2162 | 2163 | } 2164 | 2165 | return new Promise( function ( resolve, reject ) { 2166 | 2167 | loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject ); 2168 | 2169 | } ); 2170 | 2171 | } ).then( function ( texture ) { 2172 | 2173 | // Clean up resources and configure Texture. 2174 | 2175 | if ( isObjectURL === true ) { 2176 | 2177 | URL.revokeObjectURL( sourceURI ); 2178 | 2179 | } 2180 | 2181 | texture.flipY = false; 2182 | 2183 | if ( textureDef.name ) texture.name = textureDef.name; 2184 | 2185 | // Ignore unknown mime types, like DDS files. 2186 | if ( source.mimeType in MIME_TYPE_FORMATS ) { 2187 | 2188 | texture.format = MIME_TYPE_FORMATS[ source.mimeType ]; 2189 | 2190 | } 2191 | 2192 | var samplers = json.samplers || {}; 2193 | var sampler = samplers[ textureDef.sampler ] || {}; 2194 | 2195 | texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; 2196 | texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; 2197 | texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; 2198 | texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; 2199 | 2200 | return texture; 2201 | 2202 | } ); 2203 | 2204 | }; 2205 | 2206 | /** 2207 | * Asynchronously assigns a texture to the given material parameters. 2208 | * @param {Object} materialParams 2209 | * @param {string} mapName 2210 | * @param {Object} mapDef 2211 | * @return {Promise} 2212 | */ 2213 | GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) { 2214 | 2215 | var parser = this; 2216 | 2217 | return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { 2218 | 2219 | if ( ! texture.isCompressedTexture ) { 2220 | 2221 | switch ( mapName ) { 2222 | 2223 | case 'aoMap': 2224 | case 'emissiveMap': 2225 | case 'metalnessMap': 2226 | case 'normalMap': 2227 | case 'roughnessMap': 2228 | texture.format = RGBFormat; 2229 | break; 2230 | 2231 | } 2232 | 2233 | } 2234 | 2235 | // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured 2236 | // However, we will copy UV set 0 to UV set 1 on demand for aoMap 2237 | if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { 2238 | 2239 | console.warn( 'GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); 2240 | 2241 | } 2242 | 2243 | if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { 2244 | 2245 | var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; 2246 | 2247 | if ( transform ) { 2248 | 2249 | texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); 2250 | 2251 | } 2252 | 2253 | } 2254 | 2255 | materialParams[ mapName ] = texture; 2256 | 2257 | } ); 2258 | 2259 | }; 2260 | 2261 | /** 2262 | * Assigns final material to a Mesh, Line, or Points instance. The instance 2263 | * already has a material (generated from the glTF material options alone) 2264 | * but reuse of the same glTF material may require multiple threejs materials 2265 | * to accomodate different primitive types, defines, etc. New materials will 2266 | * be created if necessary, and reused from a cache. 2267 | * @param {Object3D} mesh Mesh, Line, or Points instance. 2268 | */ 2269 | GLTFParser.prototype.assignFinalMaterial = function ( mesh ) { 2270 | 2271 | var geometry = mesh.geometry; 2272 | var material = mesh.material; 2273 | 2274 | var useVertexTangents = geometry.attributes.tangent !== undefined; 2275 | var useVertexColors = geometry.attributes.color !== undefined; 2276 | var useFlatShading = geometry.attributes.normal === undefined; 2277 | var useSkinning = mesh.isSkinnedMesh === true; 2278 | var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; 2279 | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; 2280 | 2281 | if ( mesh.isPoints ) { 2282 | 2283 | var cacheKey = 'PointsMaterial:' + material.uuid; 2284 | 2285 | var pointsMaterial = this.cache.get( cacheKey ); 2286 | 2287 | if ( ! pointsMaterial ) { 2288 | 2289 | pointsMaterial = new PointsMaterial(); 2290 | Material.prototype.copy.call( pointsMaterial, material ); 2291 | pointsMaterial.color.copy( material.color ); 2292 | pointsMaterial.map = material.map; 2293 | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px 2294 | 2295 | this.cache.add( cacheKey, pointsMaterial ); 2296 | 2297 | } 2298 | 2299 | material = pointsMaterial; 2300 | 2301 | } else if ( mesh.isLine ) { 2302 | 2303 | var cacheKey = 'LineBasicMaterial:' + material.uuid; 2304 | 2305 | var lineMaterial = this.cache.get( cacheKey ); 2306 | 2307 | if ( ! lineMaterial ) { 2308 | 2309 | lineMaterial = new LineBasicMaterial(); 2310 | Material.prototype.copy.call( lineMaterial, material ); 2311 | lineMaterial.color.copy( material.color ); 2312 | 2313 | this.cache.add( cacheKey, lineMaterial ); 2314 | 2315 | } 2316 | 2317 | material = lineMaterial; 2318 | 2319 | } 2320 | 2321 | // Clone the material if it will be modified 2322 | if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { 2323 | 2324 | var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 2325 | 2326 | if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; 2327 | if ( useSkinning ) cacheKey += 'skinning:'; 2328 | if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; 2329 | if ( useVertexColors ) cacheKey += 'vertex-colors:'; 2330 | if ( useFlatShading ) cacheKey += 'flat-shading:'; 2331 | if ( useMorphTargets ) cacheKey += 'morph-targets:'; 2332 | if ( useMorphNormals ) cacheKey += 'morph-normals:'; 2333 | 2334 | var cachedMaterial = this.cache.get( cacheKey ); 2335 | 2336 | if ( ! cachedMaterial ) { 2337 | 2338 | cachedMaterial = material.clone(); 2339 | 2340 | if ( useSkinning ) cachedMaterial.skinning = true; 2341 | if ( useVertexTangents ) cachedMaterial.vertexTangents = true; 2342 | if ( useVertexColors ) cachedMaterial.vertexColors = true; 2343 | if ( useFlatShading ) cachedMaterial.flatShading = true; 2344 | if ( useMorphTargets ) cachedMaterial.morphTargets = true; 2345 | if ( useMorphNormals ) cachedMaterial.morphNormals = true; 2346 | 2347 | this.cache.add( cacheKey, cachedMaterial ); 2348 | 2349 | } 2350 | 2351 | material = cachedMaterial; 2352 | 2353 | } 2354 | 2355 | // workarounds for mesh and geometry 2356 | 2357 | if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { 2358 | 2359 | geometry.setAttribute( 'uv2', new BufferAttribute( geometry.attributes.uv.array, 2 ) ); 2360 | 2361 | } 2362 | 2363 | // https://github.com/mrdoob/js/issues/11438#issuecomment-507003995 2364 | if ( material.normalScale && ! useVertexTangents ) { 2365 | 2366 | material.normalScale.y = - material.normalScale.y; 2367 | 2368 | } 2369 | 2370 | if ( material.clearcoatNormalScale && ! useVertexTangents ) { 2371 | 2372 | material.clearcoatNormalScale.y = - material.clearcoatNormalScale.y; 2373 | 2374 | } 2375 | 2376 | mesh.material = material; 2377 | 2378 | }; 2379 | 2380 | /** 2381 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 2382 | * @param {number} materialIndex 2383 | * @return {Promise} 2384 | */ 2385 | GLTFParser.prototype.loadMaterial = function ( materialIndex ) { 2386 | 2387 | var parser = this; 2388 | var json = this.json; 2389 | var extensions = this.extensions; 2390 | var materialDef = json.materials[ materialIndex ]; 2391 | 2392 | var materialType; 2393 | var materialParams = {}; 2394 | var materialExtensions = materialDef.extensions || {}; 2395 | 2396 | var pending = []; 2397 | 2398 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { 2399 | 2400 | var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; 2401 | materialType = sgExtension.getMaterialType(); 2402 | pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); 2403 | 2404 | } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { 2405 | 2406 | var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; 2407 | materialType = kmuExtension.getMaterialType(); 2408 | pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); 2409 | 2410 | } else { 2411 | 2412 | // Specification: 2413 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 2414 | 2415 | materialType = MeshStandardMaterial; 2416 | 2417 | var metallicRoughness = materialDef.pbrMetallicRoughness || {}; 2418 | 2419 | materialParams.color = new Color( 1.0, 1.0, 1.0 ); 2420 | materialParams.opacity = 1.0; 2421 | 2422 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 2423 | 2424 | var array = metallicRoughness.baseColorFactor; 2425 | 2426 | materialParams.color.fromArray( array ); 2427 | materialParams.opacity = array[ 3 ]; 2428 | 2429 | } 2430 | 2431 | if ( metallicRoughness.baseColorTexture !== undefined ) { 2432 | 2433 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 2434 | 2435 | } 2436 | 2437 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 2438 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 2439 | 2440 | if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { 2441 | 2442 | pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2443 | pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2444 | 2445 | } 2446 | 2447 | } 2448 | 2449 | if ( materialDef.doubleSided === true ) { 2450 | 2451 | materialParams.side = DoubleSide; 2452 | 2453 | } 2454 | 2455 | var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; 2456 | 2457 | if ( alphaMode === ALPHA_MODES.BLEND ) { 2458 | 2459 | materialParams.transparent = true; 2460 | 2461 | // See: https://github.com/mrdoob/js/issues/17706 2462 | materialParams.depthWrite = false; 2463 | 2464 | } else { 2465 | 2466 | materialParams.transparent = false; 2467 | 2468 | if ( alphaMode === ALPHA_MODES.MASK ) { 2469 | 2470 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 2471 | 2472 | } 2473 | 2474 | } 2475 | 2476 | if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { 2477 | 2478 | pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); 2479 | 2480 | materialParams.normalScale = new Vector2( 1, 1 ); 2481 | 2482 | if ( materialDef.normalTexture.scale !== undefined ) { 2483 | 2484 | materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); 2485 | 2486 | } 2487 | 2488 | } 2489 | 2490 | if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { 2491 | 2492 | pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); 2493 | 2494 | if ( materialDef.occlusionTexture.strength !== undefined ) { 2495 | 2496 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 2497 | 2498 | } 2499 | 2500 | } 2501 | 2502 | if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { 2503 | 2504 | materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor ); 2505 | 2506 | } 2507 | 2508 | if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { 2509 | 2510 | pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); 2511 | 2512 | } 2513 | 2514 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_CLEARCOAT ] ) { 2515 | 2516 | var clearcoatExtension = extensions[ EXTENSIONS.KHR_MATERIALS_CLEARCOAT ]; 2517 | materialType = clearcoatExtension.getMaterialType(); 2518 | pending.push( clearcoatExtension.extendParams( materialParams, { extensions: materialExtensions }, parser ) ); 2519 | 2520 | } 2521 | 2522 | return Promise.all( pending ).then( function () { 2523 | 2524 | var material; 2525 | 2526 | if ( materialType === GLTFMeshStandardSGMaterial ) { 2527 | 2528 | material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); 2529 | 2530 | } else { 2531 | 2532 | material = new materialType( materialParams ); 2533 | 2534 | } 2535 | 2536 | if ( materialDef.name ) material.name = materialDef.name; 2537 | 2538 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 2539 | if ( material.map ) material.map.encoding = sRGBEncoding; 2540 | if ( material.emissiveMap ) material.emissiveMap.encoding = sRGBEncoding; 2541 | 2542 | assignExtrasToUserData( material, materialDef ); 2543 | 2544 | if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); 2545 | 2546 | return material; 2547 | 2548 | } ); 2549 | 2550 | }; 2551 | 2552 | /** 2553 | * @param {BufferGeometry} geometry 2554 | * @param {GLTF.Primitive} primitiveDef 2555 | * @param {GLTFParser} parser 2556 | */ 2557 | function computeBounds( geometry, primitiveDef, parser ) { 2558 | 2559 | var attributes = primitiveDef.attributes; 2560 | 2561 | var box = new Box3(); 2562 | 2563 | if ( attributes.POSITION !== undefined ) { 2564 | 2565 | var accessor = parser.json.accessors[ attributes.POSITION ]; 2566 | 2567 | var min = accessor.min; 2568 | var max = accessor.max; 2569 | 2570 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 2571 | 2572 | if ( min !== undefined && max !== undefined ) { 2573 | 2574 | box.set( 2575 | new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), 2576 | new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) ); 2577 | 2578 | } else { 2579 | 2580 | console.warn( 'GLTFLoader: Missing min/max properties for accessor POSITION.' ); 2581 | 2582 | return; 2583 | 2584 | } 2585 | 2586 | } else { 2587 | 2588 | return; 2589 | 2590 | } 2591 | 2592 | var targets = primitiveDef.targets; 2593 | 2594 | if ( targets !== undefined ) { 2595 | 2596 | var maxDisplacement = new Vector3(); 2597 | var vector = new Vector3(); 2598 | 2599 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 2600 | 2601 | var target = targets[ i ]; 2602 | 2603 | if ( target.POSITION !== undefined ) { 2604 | 2605 | var accessor = parser.json.accessors[ target.POSITION ]; 2606 | var min = accessor.min; 2607 | var max = accessor.max; 2608 | 2609 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 2610 | 2611 | if ( min !== undefined && max !== undefined ) { 2612 | 2613 | // we need to get max of absolute components because target weight is [-1,1] 2614 | vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); 2615 | vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); 2616 | vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); 2617 | 2618 | // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative 2619 | // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets 2620 | // are used to implement key-frame animations and as such only two are active at a time - this results in very large 2621 | // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. 2622 | maxDisplacement.max( vector ); 2623 | 2624 | } else { 2625 | 2626 | console.warn( 'GLTFLoader: Missing min/max properties for accessor POSITION.' ); 2627 | 2628 | } 2629 | 2630 | } 2631 | 2632 | } 2633 | 2634 | // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. 2635 | box.expandByVector( maxDisplacement ); 2636 | 2637 | } 2638 | 2639 | geometry.boundingBox = box; 2640 | 2641 | var sphere = new Sphere(); 2642 | 2643 | box.getCenter( sphere.center ); 2644 | sphere.radius = box.min.distanceTo( box.max ) / 2; 2645 | 2646 | geometry.boundingSphere = sphere; 2647 | 2648 | } 2649 | 2650 | /** 2651 | * @param {BufferGeometry} geometry 2652 | * @param {GLTF.Primitive} primitiveDef 2653 | * @param {GLTFParser} parser 2654 | * @return {Promise} 2655 | */ 2656 | function addPrimitiveAttributes( geometry, primitiveDef, parser ) { 2657 | 2658 | var attributes = primitiveDef.attributes; 2659 | 2660 | var pending = []; 2661 | 2662 | function assignAttributeAccessor( accessorIndex, attributeName ) { 2663 | 2664 | return parser.getDependency( 'accessor', accessorIndex ) 2665 | .then( function ( accessor ) { 2666 | 2667 | geometry.setAttribute( attributeName, accessor ); 2668 | 2669 | } ); 2670 | 2671 | } 2672 | 2673 | for ( var gltfAttributeName in attributes ) { 2674 | 2675 | var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); 2676 | 2677 | // Skip attributes already provided by e.g. Draco extension. 2678 | if ( threeAttributeName in geometry.attributes ) continue; 2679 | 2680 | pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); 2681 | 2682 | } 2683 | 2684 | if ( primitiveDef.indices !== undefined && ! geometry.index ) { 2685 | 2686 | var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { 2687 | 2688 | geometry.setIndex( accessor ); 2689 | 2690 | } ); 2691 | 2692 | pending.push( accessor ); 2693 | 2694 | } 2695 | 2696 | assignExtrasToUserData( geometry, primitiveDef ); 2697 | 2698 | computeBounds( geometry, primitiveDef, parser ); 2699 | 2700 | return Promise.all( pending ).then( function () { 2701 | 2702 | return primitiveDef.targets !== undefined 2703 | ? addMorphTargets( geometry, primitiveDef.targets, parser ) 2704 | : geometry; 2705 | 2706 | } ); 2707 | 2708 | } 2709 | 2710 | /** 2711 | * @param {BufferGeometry} geometry 2712 | * @param {Number} drawMode 2713 | * @return {BufferGeometry} 2714 | */ 2715 | function toTrianglesDrawMode( geometry, drawMode ) { 2716 | 2717 | var index = geometry.getIndex(); 2718 | 2719 | // generate index if not present 2720 | 2721 | if ( index === null ) { 2722 | 2723 | var indices = []; 2724 | 2725 | var position = geometry.getAttribute( 'position' ); 2726 | 2727 | if ( position !== undefined ) { 2728 | 2729 | for ( var i = 0; i < position.count; i ++ ) { 2730 | 2731 | indices.push( i ); 2732 | 2733 | } 2734 | 2735 | geometry.setIndex( indices ); 2736 | index = geometry.getIndex(); 2737 | 2738 | } else { 2739 | 2740 | console.error( 'GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 2741 | return geometry; 2742 | 2743 | } 2744 | 2745 | } 2746 | 2747 | // 2748 | 2749 | var numberOfTriangles = index.count - 2; 2750 | var newIndices = []; 2751 | 2752 | if ( drawMode === TriangleFanDrawMode ) { 2753 | 2754 | // gl.TRIANGLE_FAN 2755 | 2756 | for ( var i = 1; i <= numberOfTriangles; i ++ ) { 2757 | 2758 | newIndices.push( index.getX( 0 ) ); 2759 | newIndices.push( index.getX( i ) ); 2760 | newIndices.push( index.getX( i + 1 ) ); 2761 | 2762 | } 2763 | 2764 | } else { 2765 | 2766 | // gl.TRIANGLE_STRIP 2767 | 2768 | for ( var i = 0; i < numberOfTriangles; i ++ ) { 2769 | 2770 | if ( i % 2 === 0 ) { 2771 | 2772 | newIndices.push( index.getX( i ) ); 2773 | newIndices.push( index.getX( i + 1 ) ); 2774 | newIndices.push( index.getX( i + 2 ) ); 2775 | 2776 | 2777 | } else { 2778 | 2779 | newIndices.push( index.getX( i + 2 ) ); 2780 | newIndices.push( index.getX( i + 1 ) ); 2781 | newIndices.push( index.getX( i ) ); 2782 | 2783 | } 2784 | 2785 | } 2786 | 2787 | } 2788 | 2789 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { 2790 | 2791 | console.error( 'GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 2792 | 2793 | } 2794 | 2795 | // build final geometry 2796 | 2797 | var newGeometry = geometry.clone(); 2798 | newGeometry.setIndex( newIndices ); 2799 | 2800 | return newGeometry; 2801 | 2802 | } 2803 | 2804 | /** 2805 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2806 | * 2807 | * Creates BufferGeometries from primitives. 2808 | * 2809 | * @param {Array} primitives 2810 | * @return {Promise>} 2811 | */ 2812 | GLTFParser.prototype.loadGeometries = function ( primitives ) { 2813 | 2814 | var parser = this; 2815 | var extensions = this.extensions; 2816 | var cache = this.primitiveCache; 2817 | 2818 | function createDracoPrimitive( primitive ) { 2819 | 2820 | return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] 2821 | .decodePrimitive( primitive, parser ) 2822 | .then( function ( geometry ) { 2823 | 2824 | return addPrimitiveAttributes( geometry, primitive, parser ); 2825 | 2826 | } ); 2827 | 2828 | } 2829 | 2830 | var pending = []; 2831 | 2832 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2833 | 2834 | var primitive = primitives[ i ]; 2835 | var cacheKey = createPrimitiveKey( primitive ); 2836 | 2837 | // See if we've already created this geometry 2838 | var cached = cache[ cacheKey ]; 2839 | 2840 | if ( cached ) { 2841 | 2842 | // Use the cached geometry if it exists 2843 | pending.push( cached.promise ); 2844 | 2845 | } else { 2846 | 2847 | var geometryPromise; 2848 | 2849 | if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { 2850 | 2851 | // Use DRACO geometry if available 2852 | geometryPromise = createDracoPrimitive( primitive ); 2853 | 2854 | } else { 2855 | 2856 | // Otherwise create a new geometry 2857 | geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); 2858 | 2859 | } 2860 | 2861 | // Cache this geometry 2862 | cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; 2863 | 2864 | pending.push( geometryPromise ); 2865 | 2866 | } 2867 | 2868 | } 2869 | 2870 | return Promise.all( pending ); 2871 | 2872 | }; 2873 | 2874 | /** 2875 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2876 | * @param {number} meshIndex 2877 | * @return {Promise} 2878 | */ 2879 | GLTFParser.prototype.loadMesh = function ( meshIndex ) { 2880 | 2881 | var parser = this; 2882 | var json = this.json; 2883 | 2884 | var meshDef = json.meshes[ meshIndex ]; 2885 | var primitives = meshDef.primitives; 2886 | 2887 | var pending = []; 2888 | 2889 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2890 | 2891 | var material = primitives[ i ].material === undefined 2892 | ? createDefaultMaterial( this.cache ) 2893 | : this.getDependency( 'material', primitives[ i ].material ); 2894 | 2895 | pending.push( material ); 2896 | 2897 | } 2898 | 2899 | pending.push( parser.loadGeometries( primitives ) ); 2900 | 2901 | return Promise.all( pending ).then( function ( results ) { 2902 | 2903 | var materials = results.slice( 0, results.length - 1 ); 2904 | var geometries = results[ results.length - 1 ]; 2905 | 2906 | var meshes = []; 2907 | 2908 | for ( var i = 0, il = geometries.length; i < il; i ++ ) { 2909 | 2910 | var geometry = geometries[ i ]; 2911 | var primitive = primitives[ i ]; 2912 | 2913 | // 1. create Mesh 2914 | 2915 | var mesh; 2916 | 2917 | var material = materials[ i ]; 2918 | 2919 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 2920 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 2921 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 2922 | primitive.mode === undefined ) { 2923 | 2924 | // .isSkinnedMesh isn't in glTF spec. See .markDefs() 2925 | mesh = meshDef.isSkinnedMesh === true 2926 | ? new SkinnedMesh( geometry, material ) 2927 | : new Mesh( geometry, material ); 2928 | 2929 | if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { 2930 | 2931 | // we normalize floating point skin weight array to fix malformed assets (see #15319) 2932 | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs 2933 | mesh.normalizeSkinWeights(); 2934 | 2935 | } 2936 | 2937 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { 2938 | 2939 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); 2940 | 2941 | } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { 2942 | 2943 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); 2944 | 2945 | } 2946 | 2947 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { 2948 | 2949 | mesh = new LineSegments( geometry, material ); 2950 | 2951 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { 2952 | 2953 | mesh = new Line( geometry, material ); 2954 | 2955 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { 2956 | 2957 | mesh = new LineLoop( geometry, material ); 2958 | 2959 | } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { 2960 | 2961 | mesh = new Points( geometry, material ); 2962 | 2963 | } else { 2964 | 2965 | throw new Error( 'GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); 2966 | 2967 | } 2968 | 2969 | if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { 2970 | 2971 | updateMorphTargets( mesh, meshDef ); 2972 | 2973 | } 2974 | 2975 | mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); 2976 | 2977 | if ( geometries.length > 1 ) mesh.name += '_' + i; 2978 | 2979 | assignExtrasToUserData( mesh, meshDef ); 2980 | 2981 | parser.assignFinalMaterial( mesh ); 2982 | 2983 | meshes.push( mesh ); 2984 | 2985 | } 2986 | 2987 | if ( meshes.length === 1 ) { 2988 | 2989 | return meshes[ 0 ]; 2990 | 2991 | } 2992 | 2993 | var group = new Group(); 2994 | 2995 | for ( var i = 0, il = meshes.length; i < il; i ++ ) { 2996 | 2997 | group.add( meshes[ i ] ); 2998 | 2999 | } 3000 | 3001 | return group; 3002 | 3003 | } ); 3004 | 3005 | }; 3006 | 3007 | /** 3008 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 3009 | * @param {number} cameraIndex 3010 | * @return {Promise} 3011 | */ 3012 | GLTFParser.prototype.loadCamera = function ( cameraIndex ) { 3013 | 3014 | var camera; 3015 | var cameraDef = this.json.cameras[ cameraIndex ]; 3016 | var params = cameraDef[ cameraDef.type ]; 3017 | 3018 | if ( ! params ) { 3019 | 3020 | console.warn( 'GLTFLoader: Missing camera parameters.' ); 3021 | return; 3022 | 3023 | } 3024 | 3025 | if ( cameraDef.type === 'perspective' ) { 3026 | 3027 | camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); 3028 | 3029 | } else if ( cameraDef.type === 'orthographic' ) { 3030 | 3031 | camera = new OrthographicCamera( params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar ); 3032 | 3033 | } 3034 | 3035 | if ( cameraDef.name ) camera.name = cameraDef.name; 3036 | 3037 | assignExtrasToUserData( camera, cameraDef ); 3038 | 3039 | return Promise.resolve( camera ); 3040 | 3041 | }; 3042 | 3043 | /** 3044 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 3045 | * @param {number} skinIndex 3046 | * @return {Promise} 3047 | */ 3048 | GLTFParser.prototype.loadSkin = function ( skinIndex ) { 3049 | 3050 | var skinDef = this.json.skins[ skinIndex ]; 3051 | 3052 | var skinEntry = { joints: skinDef.joints }; 3053 | 3054 | if ( skinDef.inverseBindMatrices === undefined ) { 3055 | 3056 | return Promise.resolve( skinEntry ); 3057 | 3058 | } 3059 | 3060 | return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { 3061 | 3062 | skinEntry.inverseBindMatrices = accessor; 3063 | 3064 | return skinEntry; 3065 | 3066 | } ); 3067 | 3068 | }; 3069 | 3070 | /** 3071 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 3072 | * @param {number} animationIndex 3073 | * @return {Promise} 3074 | */ 3075 | GLTFParser.prototype.loadAnimation = function ( animationIndex ) { 3076 | 3077 | var json = this.json; 3078 | 3079 | var animationDef = json.animations[ animationIndex ]; 3080 | 3081 | var pendingNodes = []; 3082 | var pendingInputAccessors = []; 3083 | var pendingOutputAccessors = []; 3084 | var pendingSamplers = []; 3085 | var pendingTargets = []; 3086 | 3087 | for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { 3088 | 3089 | var channel = animationDef.channels[ i ]; 3090 | var sampler = animationDef.samplers[ channel.sampler ]; 3091 | var target = channel.target; 3092 | var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 3093 | var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; 3094 | var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; 3095 | 3096 | pendingNodes.push( this.getDependency( 'node', name ) ); 3097 | pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); 3098 | pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); 3099 | pendingSamplers.push( sampler ); 3100 | pendingTargets.push( target ); 3101 | 3102 | } 3103 | 3104 | return Promise.all( [ 3105 | 3106 | Promise.all( pendingNodes ), 3107 | Promise.all( pendingInputAccessors ), 3108 | Promise.all( pendingOutputAccessors ), 3109 | Promise.all( pendingSamplers ), 3110 | Promise.all( pendingTargets ) 3111 | 3112 | ] ).then( function ( dependencies ) { 3113 | 3114 | var nodes = dependencies[ 0 ]; 3115 | var inputAccessors = dependencies[ 1 ]; 3116 | var outputAccessors = dependencies[ 2 ]; 3117 | var samplers = dependencies[ 3 ]; 3118 | var targets = dependencies[ 4 ]; 3119 | 3120 | var tracks = []; 3121 | 3122 | for ( var i = 0, il = nodes.length; i < il; i ++ ) { 3123 | 3124 | var node = nodes[ i ]; 3125 | var inputAccessor = inputAccessors[ i ]; 3126 | var outputAccessor = outputAccessors[ i ]; 3127 | var sampler = samplers[ i ]; 3128 | var target = targets[ i ]; 3129 | 3130 | if ( node === undefined ) continue; 3131 | 3132 | node.updateMatrix(); 3133 | node.matrixAutoUpdate = true; 3134 | 3135 | var TypedKeyframeTrack; 3136 | 3137 | switch ( PATH_PROPERTIES[ target.path ] ) { 3138 | 3139 | case PATH_PROPERTIES.weights: 3140 | 3141 | TypedKeyframeTrack = NumberKeyframeTrack; 3142 | break; 3143 | 3144 | case PATH_PROPERTIES.rotation: 3145 | 3146 | TypedKeyframeTrack = QuaternionKeyframeTrack; 3147 | break; 3148 | 3149 | case PATH_PROPERTIES.position: 3150 | case PATH_PROPERTIES.scale: 3151 | default: 3152 | 3153 | TypedKeyframeTrack = VectorKeyframeTrack; 3154 | break; 3155 | 3156 | } 3157 | 3158 | var targetName = node.name ? node.name : node.uuid; 3159 | 3160 | var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; 3161 | 3162 | var targetNames = []; 3163 | 3164 | if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { 3165 | 3166 | // Node may be a Group (glTF mesh with several primitives) or a Mesh. 3167 | node.traverse( function ( object ) { 3168 | 3169 | if ( object.isMesh === true && object.morphTargetInfluences ) { 3170 | 3171 | targetNames.push( object.name ? object.name : object.uuid ); 3172 | 3173 | } 3174 | 3175 | } ); 3176 | 3177 | } else { 3178 | 3179 | targetNames.push( targetName ); 3180 | 3181 | } 3182 | 3183 | var outputArray = outputAccessor.array; 3184 | 3185 | if ( outputAccessor.normalized ) { 3186 | 3187 | var scale; 3188 | 3189 | if ( outputArray.constructor === Int8Array ) { 3190 | 3191 | scale = 1 / 127; 3192 | 3193 | } else if ( outputArray.constructor === Uint8Array ) { 3194 | 3195 | scale = 1 / 255; 3196 | 3197 | } else if ( outputArray.constructor == Int16Array ) { 3198 | 3199 | scale = 1 / 32767; 3200 | 3201 | } else if ( outputArray.constructor === Uint16Array ) { 3202 | 3203 | scale = 1 / 65535; 3204 | 3205 | } else { 3206 | 3207 | throw new Error( 'GLTFLoader: Unsupported output accessor component type.' ); 3208 | 3209 | } 3210 | 3211 | var scaled = new Float32Array( outputArray.length ); 3212 | 3213 | for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) { 3214 | 3215 | scaled[ j ] = outputArray[ j ] * scale; 3216 | 3217 | } 3218 | 3219 | outputArray = scaled; 3220 | 3221 | } 3222 | 3223 | for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { 3224 | 3225 | var track = new TypedKeyframeTrack( 3226 | targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], 3227 | inputAccessor.array, 3228 | outputArray, 3229 | interpolation 3230 | ); 3231 | 3232 | // Override interpolation with custom factory method. 3233 | if ( sampler.interpolation === 'CUBICSPLINE' ) { 3234 | 3235 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { 3236 | 3237 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 3238 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 3239 | // must be divided by three to get the interpolant's sampleSize argument. 3240 | 3241 | return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); 3242 | 3243 | }; 3244 | 3245 | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 3246 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 3247 | 3248 | } 3249 | 3250 | tracks.push( track ); 3251 | 3252 | } 3253 | 3254 | } 3255 | 3256 | var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; 3257 | 3258 | return new AnimationClip( name, undefined, tracks ); 3259 | 3260 | } ); 3261 | 3262 | }; 3263 | 3264 | /** 3265 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 3266 | * @param {number} nodeIndex 3267 | * @return {Promise} 3268 | */ 3269 | GLTFParser.prototype.loadNode = function ( nodeIndex ) { 3270 | 3271 | var json = this.json; 3272 | var extensions = this.extensions; 3273 | var parser = this; 3274 | 3275 | var meshReferences = json.meshReferences; 3276 | var meshUses = json.meshUses; 3277 | 3278 | var nodeDef = json.nodes[ nodeIndex ]; 3279 | 3280 | return ( function () { 3281 | 3282 | var pending = []; 3283 | 3284 | if ( nodeDef.mesh !== undefined ) { 3285 | 3286 | pending.push( parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { 3287 | 3288 | var node; 3289 | 3290 | if ( meshReferences[ nodeDef.mesh ] > 1 ) { 3291 | 3292 | var instanceNum = meshUses[ nodeDef.mesh ] ++; 3293 | 3294 | node = mesh.clone(); 3295 | node.name += '_instance_' + instanceNum; 3296 | 3297 | } else { 3298 | 3299 | node = mesh; 3300 | 3301 | } 3302 | 3303 | // if weights are provided on the node, override weights on the mesh. 3304 | if ( nodeDef.weights !== undefined ) { 3305 | 3306 | node.traverse( function ( o ) { 3307 | 3308 | if ( ! o.isMesh ) return; 3309 | 3310 | for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) { 3311 | 3312 | o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; 3313 | 3314 | } 3315 | 3316 | } ); 3317 | 3318 | } 3319 | 3320 | return node; 3321 | 3322 | } ) ); 3323 | 3324 | } else { 3325 | 3326 | // no mesh 3327 | if ( nodeDef.extensions 3328 | && nodeDef.extensions[ EXTENSIONS.MOZ_TEXT ] 3329 | && nodeDef.extensions[ EXTENSIONS.MOZ_TEXT ].index !== undefined ) { 3330 | 3331 | pending.push( parser.getDependency( 'text', nodeDef.extensions[ EXTENSIONS.MOZ_TEXT ].index ) ); 3332 | 3333 | } 3334 | } 3335 | 3336 | if ( nodeDef.camera !== undefined ) { 3337 | 3338 | pending.push( parser.getDependency( 'camera', nodeDef.camera ) ); 3339 | 3340 | } 3341 | 3342 | if ( nodeDef.extensions 3343 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] 3344 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light !== undefined ) { 3345 | 3346 | pending.push( parser.getDependency( 'light', nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light ) ); 3347 | 3348 | } 3349 | 3350 | return Promise.all( pending ); 3351 | 3352 | }() ).then( function ( objects ) { 3353 | 3354 | var node; 3355 | 3356 | // .isBone isn't in glTF spec. See .markDefs 3357 | if ( nodeDef.isBone === true ) { 3358 | 3359 | node = new Bone(); 3360 | 3361 | } else if ( objects.length > 1 ) { 3362 | 3363 | node = new Group(); 3364 | 3365 | } else if ( objects.length === 1 ) { 3366 | 3367 | node = objects[ 0 ]; 3368 | 3369 | } else { 3370 | 3371 | node = new Object3D(); 3372 | 3373 | } 3374 | 3375 | if ( node !== objects[ 0 ] ) { 3376 | 3377 | for ( var i = 0, il = objects.length; i < il; i ++ ) { 3378 | 3379 | node.add( objects[ i ] ); 3380 | 3381 | } 3382 | 3383 | } 3384 | 3385 | if ( nodeDef.name ) { 3386 | 3387 | node.userData.name = nodeDef.name; 3388 | node.name = PropertyBinding.sanitizeNodeName( nodeDef.name ); 3389 | 3390 | } 3391 | 3392 | assignExtrasToUserData( node, nodeDef ); 3393 | 3394 | if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); 3395 | 3396 | if ( nodeDef.matrix !== undefined ) { 3397 | 3398 | var matrix = new Matrix4(); 3399 | matrix.fromArray( nodeDef.matrix ); 3400 | node.applyMatrix4( matrix ); 3401 | 3402 | } else { 3403 | 3404 | if ( nodeDef.translation !== undefined ) { 3405 | 3406 | node.position.fromArray( nodeDef.translation ); 3407 | 3408 | } 3409 | 3410 | if ( nodeDef.rotation !== undefined ) { 3411 | 3412 | node.quaternion.fromArray( nodeDef.rotation ); 3413 | 3414 | } 3415 | 3416 | if ( nodeDef.scale !== undefined ) { 3417 | 3418 | node.scale.fromArray( nodeDef.scale ); 3419 | 3420 | } 3421 | 3422 | } 3423 | 3424 | return node; 3425 | 3426 | } ); 3427 | 3428 | }; 3429 | 3430 | /** 3431 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3432 | * @param {number} sceneIndex 3433 | * @return {Promise} 3434 | */ 3435 | GLTFParser.prototype.loadScene = function () { 3436 | 3437 | // scene node hierachy builder 3438 | 3439 | function buildNodeHierachy( nodeId, parentObject, json, parser ) { 3440 | 3441 | var nodeDef = json.nodes[ nodeId ]; 3442 | 3443 | return parser.getDependency( 'node', nodeId ).then( function ( node ) { 3444 | 3445 | if ( nodeDef.skin === undefined ) return node; 3446 | 3447 | // build skeleton here as well 3448 | 3449 | var skinEntry; 3450 | 3451 | return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { 3452 | 3453 | skinEntry = skin; 3454 | 3455 | var pendingJoints = []; 3456 | 3457 | for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) { 3458 | 3459 | pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); 3460 | 3461 | } 3462 | 3463 | return Promise.all( pendingJoints ); 3464 | 3465 | } ).then( function ( jointNodes ) { 3466 | 3467 | node.traverse( function ( mesh ) { 3468 | 3469 | if ( ! mesh.isMesh ) return; 3470 | 3471 | var bones = []; 3472 | var boneInverses = []; 3473 | 3474 | for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) { 3475 | 3476 | var jointNode = jointNodes[ j ]; 3477 | 3478 | if ( jointNode ) { 3479 | 3480 | bones.push( jointNode ); 3481 | 3482 | var mat = new Matrix4(); 3483 | 3484 | if ( skinEntry.inverseBindMatrices !== undefined ) { 3485 | 3486 | mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); 3487 | 3488 | } 3489 | 3490 | boneInverses.push( mat ); 3491 | 3492 | } else { 3493 | 3494 | console.warn( 'GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); 3495 | 3496 | } 3497 | 3498 | } 3499 | 3500 | mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld ); 3501 | 3502 | } ); 3503 | 3504 | return node; 3505 | 3506 | } ); 3507 | 3508 | } ).then( function ( node ) { 3509 | 3510 | // build node hierachy 3511 | 3512 | parentObject.add( node ); 3513 | 3514 | var pending = []; 3515 | 3516 | if ( nodeDef.children ) { 3517 | 3518 | var children = nodeDef.children; 3519 | 3520 | for ( var i = 0, il = children.length; i < il; i ++ ) { 3521 | 3522 | var child = children[ i ]; 3523 | pending.push( buildNodeHierachy( child, node, json, parser ) ); 3524 | 3525 | } 3526 | 3527 | } 3528 | 3529 | return Promise.all( pending ); 3530 | 3531 | } ); 3532 | 3533 | } 3534 | 3535 | return function loadScene( sceneIndex ) { 3536 | 3537 | var json = this.json; 3538 | var extensions = this.extensions; 3539 | var sceneDef = this.json.scenes[ sceneIndex ]; 3540 | var parser = this; 3541 | 3542 | // Loader returns Group, not Scene. 3543 | // See: https://github.com/mrdoob/js/issues/18342#issuecomment-578981172 3544 | var scene = new Group(); 3545 | if ( sceneDef.name ) scene.name = sceneDef.name; 3546 | 3547 | assignExtrasToUserData( scene, sceneDef ); 3548 | 3549 | if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); 3550 | 3551 | var nodeIds = sceneDef.nodes || []; 3552 | 3553 | var pending = []; 3554 | 3555 | for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { 3556 | 3557 | pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); 3558 | 3559 | } 3560 | 3561 | return Promise.all( pending ).then( function () { 3562 | 3563 | return scene; 3564 | 3565 | } ); 3566 | 3567 | }; 3568 | 3569 | }(); 3570 | 3571 | return GLTFLoader; 3572 | 3573 | } )(); 3574 | 3575 | export { GLTFLoader }; 3576 | -------------------------------------------------------------------------------- /test/blendfiles/align.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/align.blend -------------------------------------------------------------------------------- /test/blendfiles/basic.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/basic.blend -------------------------------------------------------------------------------- /test/blendfiles/letterspacing.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/letterspacing.blend -------------------------------------------------------------------------------- /test/blendfiles/lineheight.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/lineheight.blend -------------------------------------------------------------------------------- /test/blendfiles/lorem ipsun.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/lorem ipsun.blend -------------------------------------------------------------------------------- /test/blendfiles/materials.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/materials.blend -------------------------------------------------------------------------------- /test/blendfiles/overflow.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/overflow.blend -------------------------------------------------------------------------------- /test/blendfiles/sizes.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/sizes.blend -------------------------------------------------------------------------------- /test/blendfiles/types.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/blendfiles/types.blend -------------------------------------------------------------------------------- /test/fonts/Arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/fonts/Arial.ttf -------------------------------------------------------------------------------- /test/fonts/Comic Sans MS Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/fonts/Comic Sans MS Bold.ttf -------------------------------------------------------------------------------- /test/fonts/Georgia.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/fonts/Georgia.ttf -------------------------------------------------------------------------------- /test/fonts/Impact.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/MOZ_text/5672b7c08b8106d8930d45b540b5cb67eaec879f/test/fonts/Impact.ttf -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MOZ_text tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {OrbitControls} from './node_modules/three/examples/jsm/controls/OrbitControls.js' 3 | 4 | import {GLTFLoader} from "./GLTFLoader.module.js"; 5 | import * as dat from 'dat.gui'; 6 | 7 | const CAMERA_DISTANCE = 30; 8 | var gui, scene, camera, camera1, camera2, renderer, model, controls1, controls2, controls; 9 | 10 | window.onload = () => { init() }; 11 | 12 | function init() { 13 | scene = new THREE.Scene(); 14 | camera1 = new THREE.OrthographicCamera( 15 | window.innerWidth / - 100, 16 | window.innerWidth / 100, 17 | window.innerHeight / 100, 18 | window.innerHeight / - 100, 19 | 1, 1000 ); 20 | camera2 = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 0.1, 100); 21 | camera1.position.set(0, 0, CAMERA_DISTANCE); 22 | camera2.position.set(0, 0, CAMERA_DISTANCE); 23 | camera = camera1; 24 | renderer = new THREE.WebGLRenderer(); 25 | renderer.setSize(window.innerWidth, window.innerHeight); 26 | document.body.appendChild(renderer.domElement); 27 | renderer.outputEncoding = THREE.sRGBEncoding; 28 | renderer.setClearColor(new THREE.Color(0.3, 0.3, 0.3), 1.0); 29 | renderer.setAnimationLoop(renderLoop); 30 | 31 | const light = new THREE.PointLight(); 32 | light.position.set(10, 40, 50); 33 | scene.add(light); 34 | 35 | controls1 = new OrbitControls(camera1, renderer.domElement); 36 | controls2 = new OrbitControls(camera2, renderer.domElement); 37 | controls = controls1 38 | 39 | var ControlsData = function() { 40 | this.ortho = true; 41 | this.scene = 'basic'; 42 | this.SCENES = [ 43 | 'align', 44 | 'letterspacing', 45 | 'materials', 46 | 'sizes', 47 | 'basic', 48 | 'lineheight', 49 | 'overflow', 50 | 'types', 51 | 'lorem ipsun' 52 | ]; 53 | this.ResetCamera = () => { 54 | camera1.position.set(0, 0, CAMERA_DISTANCE); 55 | camera2.position.set(0, 0, CAMERA_DISTANCE); 56 | } 57 | }; 58 | 59 | var controlsData = new ControlsData(); 60 | gui = new dat.GUI(); 61 | gui.add(controlsData, 'scene', controlsData.SCENES).listen().onChange(val => { 62 | //location.hash = val; 63 | loadScene(val); 64 | }); 65 | gui.add(controlsData, 'ortho').onChange(val => { 66 | camera = val ? camera1 : camera2; 67 | controls = val ? controls1 : controls2; 68 | }); 69 | gui.add(controlsData, 'ResetCamera'); 70 | 71 | const file = location.hash ? location.hash.substr(1) : 'basic'; 72 | controlsData.scene = file; 73 | loadScene(file); 74 | } 75 | 76 | function loadScene(file) { 77 | if (model) { 78 | scene.remove(model); 79 | } 80 | new GLTFLoader().load(`models/${file}.gltf`, gltf => { 81 | model = gltf.scene; 82 | scene.add(model); 83 | for (var i = 0; i < model.children.length; i++){ 84 | var t = model.children[i]; 85 | 86 | const origin = new THREE.SphereGeometry(0.03); 87 | t.add(new THREE.Mesh(origin)); 88 | } 89 | }); 90 | } 91 | 92 | function renderLoop(t) { 93 | controls.update(); 94 | renderer.render(scene, camera); 95 | //camera.lookAt(0, 0, 0); 96 | } 97 | -------------------------------------------------------------------------------- /test/models/align.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4, 19 | 5, 20 | 6, 21 | 7, 22 | 8, 23 | 9, 24 | 10, 25 | 11, 26 | 12, 27 | 13, 28 | 14 29 | ] 30 | } 31 | ], 32 | "nodes" : [ 33 | { 34 | "extensions" : { 35 | "MOZ_text" : { 36 | "index" : 0, 37 | "type" : "sdf", 38 | "color" : [ 39 | 1.0, 40 | 1.0, 41 | 1.0, 42 | 1.0 43 | ], 44 | "alignX" : "center", 45 | "alignY" : "top", 46 | "value" : "center-top", 47 | "fontName" : "ArialMT", 48 | "fontFile" : "fonts/Arial.ttf", 49 | "size" : 1, 50 | "maxWidth" : 0, 51 | "overflow" : "NONE", 52 | "letterSpacing" : 0, 53 | "lineSpacing" : 1, 54 | "dimensions" : [ 55 | 4.25600004196167, 56 | 0.7720000147819519 57 | ] 58 | } 59 | }, 60 | "name" : "Text.001", 61 | "rotation" : [ 62 | 0.7071067690849304, 63 | 0, 64 | 0, 65 | 0.7071068286895752 66 | ] 67 | }, 68 | { 69 | "extensions" : { 70 | "MOZ_text" : { 71 | "index" : 1, 72 | "type" : "sdf", 73 | "color" : [ 74 | 1.0, 75 | 1.0, 76 | 1.0, 77 | 1.0 78 | ], 79 | "alignX" : "center", 80 | "alignY" : "bottom", 81 | "value" : "center-bottom", 82 | "fontName" : "ArialMT", 83 | "fontFile" : "fonts/Arial.ttf", 84 | "size" : 1, 85 | "maxWidth" : 0, 86 | "overflow" : "NONE", 87 | "letterSpacing" : 0, 88 | "lineSpacing" : 1, 89 | "dimensions" : [ 90 | 5.889000415802002, 91 | 0.6910000443458557 92 | ] 93 | } 94 | }, 95 | "name" : "Text.002", 96 | "rotation" : [ 97 | 0.7071067690849304, 98 | 0, 99 | 0, 100 | 0.7071068286895752 101 | ] 102 | }, 103 | { 104 | "extensions" : { 105 | "MOZ_text" : { 106 | "index" : 2, 107 | "type" : "sdf", 108 | "color" : [ 109 | 1.0, 110 | 1.0, 111 | 1.0, 112 | 1.0 113 | ], 114 | "alignX" : "center", 115 | "alignY" : "top_baseline", 116 | "value" : "center-top baseline", 117 | "fontName" : "ArialMT", 118 | "fontFile" : "fonts/Arial.ttf", 119 | "size" : 1, 120 | "maxWidth" : 0, 121 | "overflow" : "NONE", 122 | "letterSpacing" : 0, 123 | "lineSpacing" : 1, 124 | "dimensions" : [ 125 | 7.74500036239624, 126 | 0.9120000600814819 127 | ] 128 | } 129 | }, 130 | "name" : "Text.003", 131 | "rotation" : [ 132 | 0.7071067690849304, 133 | 0, 134 | 0, 135 | 0.7071068286895752 136 | ] 137 | }, 138 | { 139 | "extensions" : { 140 | "MOZ_text" : { 141 | "index" : 3, 142 | "type" : "sdf", 143 | "color" : [ 144 | 1.0, 145 | 1.0, 146 | 1.0, 147 | 1.0 148 | ], 149 | "alignX" : "center", 150 | "alignY" : "center", 151 | "value" : "center-center", 152 | "fontName" : "ArialMT", 153 | "fontFile" : "fonts/Arial.ttf", 154 | "size" : 1, 155 | "maxWidth" : 0, 156 | "overflow" : "NONE", 157 | "letterSpacing" : 0, 158 | "lineSpacing" : 1, 159 | "dimensions" : [ 160 | 5.5329999923706055, 161 | 0.5510000586509705 162 | ] 163 | } 164 | }, 165 | "name" : "Text.004", 166 | "rotation" : [ 167 | 0.7071067690849304, 168 | 0, 169 | 0, 170 | 0.7071068286895752 171 | ] 172 | }, 173 | { 174 | "extensions" : { 175 | "MOZ_text" : { 176 | "index" : 4, 177 | "type" : "sdf", 178 | "color" : [ 179 | 1.0, 180 | 1.0, 181 | 1.0, 182 | 1.0 183 | ], 184 | "alignX" : "center", 185 | "alignY" : "bottom_baseline", 186 | "value" : "center-bottom baseline", 187 | "fontName" : "ArialMT", 188 | "fontFile" : "fonts/Arial.ttf", 189 | "size" : 1, 190 | "maxWidth" : 0, 191 | "overflow" : "NONE", 192 | "letterSpacing" : 0, 193 | "lineSpacing" : 1, 194 | "dimensions" : [ 195 | 9.4010009765625, 196 | 0.6910000443458557 197 | ] 198 | } 199 | }, 200 | "name" : "Text.005", 201 | "rotation" : [ 202 | 0.7071067690849304, 203 | 0, 204 | 0, 205 | 0.7071068286895752 206 | ] 207 | }, 208 | { 209 | "extensions" : { 210 | "MOZ_text" : { 211 | "index" : 5, 212 | "type" : "sdf", 213 | "color" : [ 214 | 1.0, 215 | 1.0, 216 | 1.0, 217 | 1.0 218 | ], 219 | "alignX" : "left", 220 | "alignY" : "top", 221 | "value" : "center-top", 222 | "fontName" : "ArialMT", 223 | "fontFile" : "fonts/Arial.ttf", 224 | "size" : 1, 225 | "maxWidth" : 0, 226 | "overflow" : "NONE", 227 | "letterSpacing" : 0, 228 | "lineSpacing" : 1, 229 | "dimensions" : [ 230 | 4.25600004196167, 231 | 0.7720000147819519 232 | ] 233 | } 234 | }, 235 | "name" : "Text.006", 236 | "rotation" : [ 237 | 0.7071067690849304, 238 | 0, 239 | 0, 240 | 0.7071068286895752 241 | ], 242 | "translation" : [ 243 | -11.681417465209961, 244 | 0, 245 | 0 246 | ] 247 | }, 248 | { 249 | "extensions" : { 250 | "MOZ_text" : { 251 | "index" : 6, 252 | "type" : "sdf", 253 | "color" : [ 254 | 1.0, 255 | 1.0, 256 | 1.0, 257 | 1.0 258 | ], 259 | "alignX" : "left", 260 | "alignY" : "bottom", 261 | "value" : "center-bottom", 262 | "fontName" : "ArialMT", 263 | "fontFile" : "fonts/Arial.ttf", 264 | "size" : 1, 265 | "maxWidth" : 0, 266 | "overflow" : "NONE", 267 | "letterSpacing" : 0, 268 | "lineSpacing" : 1, 269 | "dimensions" : [ 270 | 5.889000415802002, 271 | 0.6910000443458557 272 | ] 273 | } 274 | }, 275 | "name" : "Text.007", 276 | "rotation" : [ 277 | 0.7071067690849304, 278 | 0, 279 | 0, 280 | 0.7071068286895752 281 | ], 282 | "translation" : [ 283 | -11.681417465209961, 284 | 0, 285 | 0 286 | ] 287 | }, 288 | { 289 | "extensions" : { 290 | "MOZ_text" : { 291 | "index" : 7, 292 | "type" : "sdf", 293 | "color" : [ 294 | 1.0, 295 | 1.0, 296 | 1.0, 297 | 1.0 298 | ], 299 | "alignX" : "left", 300 | "alignY" : "top_baseline", 301 | "value" : "center-top baseline", 302 | "fontName" : "ArialMT", 303 | "fontFile" : "fonts/Arial.ttf", 304 | "size" : 1, 305 | "maxWidth" : 0, 306 | "overflow" : "NONE", 307 | "letterSpacing" : 0, 308 | "lineSpacing" : 1, 309 | "dimensions" : [ 310 | 7.74500036239624, 311 | 0.9120000600814819 312 | ] 313 | } 314 | }, 315 | "name" : "Text.008", 316 | "rotation" : [ 317 | 0.7071067690849304, 318 | 0, 319 | 0, 320 | 0.7071068286895752 321 | ], 322 | "translation" : [ 323 | -11.681417465209961, 324 | 0, 325 | 0 326 | ] 327 | }, 328 | { 329 | "extensions" : { 330 | "MOZ_text" : { 331 | "index" : 8, 332 | "type" : "sdf", 333 | "color" : [ 334 | 1.0, 335 | 1.0, 336 | 1.0, 337 | 1.0 338 | ], 339 | "alignX" : "left", 340 | "alignY" : "center", 341 | "value" : "center-center", 342 | "fontName" : "ArialMT", 343 | "fontFile" : "fonts/Arial.ttf", 344 | "size" : 1, 345 | "maxWidth" : 0, 346 | "overflow" : "NONE", 347 | "letterSpacing" : 0, 348 | "lineSpacing" : 1, 349 | "dimensions" : [ 350 | 5.5329999923706055, 351 | 0.5510000586509705 352 | ] 353 | } 354 | }, 355 | "name" : "Text.009", 356 | "rotation" : [ 357 | 0.7071067690849304, 358 | 0, 359 | 0, 360 | 0.7071068286895752 361 | ], 362 | "translation" : [ 363 | -11.681417465209961, 364 | 0, 365 | 0 366 | ] 367 | }, 368 | { 369 | "extensions" : { 370 | "MOZ_text" : { 371 | "index" : 9, 372 | "type" : "sdf", 373 | "color" : [ 374 | 1.0, 375 | 1.0, 376 | 1.0, 377 | 1.0 378 | ], 379 | "alignX" : "left", 380 | "alignY" : "bottom_baseline", 381 | "value" : "center-bottom baseline", 382 | "fontName" : "ArialMT", 383 | "fontFile" : "fonts/Arial.ttf", 384 | "size" : 1, 385 | "maxWidth" : 0, 386 | "overflow" : "NONE", 387 | "letterSpacing" : 0, 388 | "lineSpacing" : 1, 389 | "dimensions" : [ 390 | 9.401000022888184, 391 | 0.6910000443458557 392 | ] 393 | } 394 | }, 395 | "name" : "Text.010", 396 | "rotation" : [ 397 | 0.7071067690849304, 398 | 0, 399 | 0, 400 | 0.7071068286895752 401 | ], 402 | "translation" : [ 403 | -11.681417465209961, 404 | 0, 405 | 0 406 | ] 407 | }, 408 | { 409 | "extensions" : { 410 | "MOZ_text" : { 411 | "index" : 10, 412 | "type" : "sdf", 413 | "color" : [ 414 | 1.0, 415 | 1.0, 416 | 1.0, 417 | 1.0 418 | ], 419 | "alignX" : "right", 420 | "alignY" : "top", 421 | "value" : "center-top", 422 | "fontName" : "ArialMT", 423 | "fontFile" : "fonts/Arial.ttf", 424 | "size" : 1, 425 | "maxWidth" : 0, 426 | "overflow" : "NONE", 427 | "letterSpacing" : 0, 428 | "lineSpacing" : 1, 429 | "dimensions" : [ 430 | 4.25600004196167, 431 | 0.7720000147819519 432 | ] 433 | } 434 | }, 435 | "name" : "Text.011", 436 | "rotation" : [ 437 | 0.7071067690849304, 438 | 0, 439 | 0, 440 | 0.7071068286895752 441 | ], 442 | "translation" : [ 443 | 11.71873664855957, 444 | 0, 445 | 0 446 | ] 447 | }, 448 | { 449 | "extensions" : { 450 | "MOZ_text" : { 451 | "index" : 11, 452 | "type" : "sdf", 453 | "color" : [ 454 | 1.0, 455 | 1.0, 456 | 1.0, 457 | 1.0 458 | ], 459 | "alignX" : "right", 460 | "alignY" : "bottom", 461 | "value" : "center-bottom", 462 | "fontName" : "ArialMT", 463 | "fontFile" : "fonts/Arial.ttf", 464 | "size" : 1, 465 | "maxWidth" : 0, 466 | "overflow" : "NONE", 467 | "letterSpacing" : 0, 468 | "lineSpacing" : 1, 469 | "dimensions" : [ 470 | 5.889000415802002, 471 | 0.6910000443458557 472 | ] 473 | } 474 | }, 475 | "name" : "Text.012", 476 | "rotation" : [ 477 | 0.7071067690849304, 478 | 0, 479 | 0, 480 | 0.7071068286895752 481 | ], 482 | "translation" : [ 483 | 11.71873664855957, 484 | 0, 485 | 0 486 | ] 487 | }, 488 | { 489 | "extensions" : { 490 | "MOZ_text" : { 491 | "index" : 12, 492 | "type" : "sdf", 493 | "color" : [ 494 | 1.0, 495 | 1.0, 496 | 1.0, 497 | 1.0 498 | ], 499 | "alignX" : "right", 500 | "alignY" : "top_baseline", 501 | "value" : "center-top baseline", 502 | "fontName" : "ArialMT", 503 | "fontFile" : "fonts/Arial.ttf", 504 | "size" : 1, 505 | "maxWidth" : 0, 506 | "overflow" : "NONE", 507 | "letterSpacing" : 0, 508 | "lineSpacing" : 1, 509 | "dimensions" : [ 510 | 7.74500036239624, 511 | 0.9120000600814819 512 | ] 513 | } 514 | }, 515 | "name" : "Text.013", 516 | "rotation" : [ 517 | 0.7071067690849304, 518 | 0, 519 | 0, 520 | 0.7071068286895752 521 | ], 522 | "translation" : [ 523 | 11.71873664855957, 524 | 0, 525 | 0 526 | ] 527 | }, 528 | { 529 | "extensions" : { 530 | "MOZ_text" : { 531 | "index" : 13, 532 | "type" : "sdf", 533 | "color" : [ 534 | 1.0, 535 | 1.0, 536 | 1.0, 537 | 1.0 538 | ], 539 | "alignX" : "right", 540 | "alignY" : "center", 541 | "value" : "center-center", 542 | "fontName" : "ArialMT", 543 | "fontFile" : "fonts/Arial.ttf", 544 | "size" : 1, 545 | "maxWidth" : 0, 546 | "overflow" : "NONE", 547 | "letterSpacing" : 0, 548 | "lineSpacing" : 1, 549 | "dimensions" : [ 550 | 5.5329999923706055, 551 | 0.5510000586509705 552 | ] 553 | } 554 | }, 555 | "name" : "Text.014", 556 | "rotation" : [ 557 | 0.7071067690849304, 558 | 0, 559 | 0, 560 | 0.7071068286895752 561 | ], 562 | "translation" : [ 563 | 11.71873664855957, 564 | 0, 565 | 0 566 | ] 567 | }, 568 | { 569 | "extensions" : { 570 | "MOZ_text" : { 571 | "index" : 14, 572 | "type" : "sdf", 573 | "color" : [ 574 | 1.0, 575 | 1.0, 576 | 1.0, 577 | 1.0 578 | ], 579 | "alignX" : "right", 580 | "alignY" : "bottom_baseline", 581 | "value" : "center-bottom baseline", 582 | "fontName" : "ArialMT", 583 | "fontFile" : "fonts/Arial.ttf", 584 | "size" : 1, 585 | "maxWidth" : 0, 586 | "overflow" : "NONE", 587 | "letterSpacing" : 0, 588 | "lineSpacing" : 1, 589 | "dimensions" : [ 590 | 9.401000022888184, 591 | 0.6910000443458557 592 | ] 593 | } 594 | }, 595 | "name" : "Text.015", 596 | "rotation" : [ 597 | 0.7071067690849304, 598 | 0, 599 | 0, 600 | 0.7071068286895752 601 | ], 602 | "translation" : [ 603 | 11.71873664855957, 604 | 0, 605 | 0 606 | ] 607 | } 608 | ] 609 | } 610 | -------------------------------------------------------------------------------- /test/models/basic.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3 18 | ] 19 | } 20 | ], 21 | "nodes" : [ 22 | { 23 | "extensions" : { 24 | "MOZ_text" : { 25 | "index" : 0, 26 | "type" : "sdf", 27 | "color" : [ 28 | 1.0, 29 | 1.0, 30 | 1.0, 31 | 1.0 32 | ], 33 | "alignX" : "center", 34 | "alignY" : "center", 35 | "value" : "Lorem\nIPSUM", 36 | "fontName" : "Impact", 37 | "fontFile" : "fonts/Impact.ttf", 38 | "size" : 1.247999906539917, 39 | "maxWidth" : 0, 40 | "overflow" : "NONE", 41 | "letterSpacing" : 0.10000002384185791, 42 | "lineSpacing" : 0.7000000476837158, 43 | "dimensions" : [ 44 | 3.4475998878479004, 45 | 1.735967993736267 46 | ] 47 | } 48 | }, 49 | "name" : "Text", 50 | "rotation" : [ 51 | 0.7071067690849304, 52 | 0, 53 | 0, 54 | 0.7071067690849304 55 | ], 56 | "translation" : [ 57 | 0, 58 | 0.3041502833366394, 59 | 0.09999999403953552 60 | ] 61 | }, 62 | { 63 | "mesh" : 0, 64 | "name" : "Plane", 65 | "rotation" : [ 66 | 0.7071067690849304, 67 | 0, 68 | 0, 69 | 0.7071067690849304 70 | ] 71 | }, 72 | { 73 | "extensions" : { 74 | "MOZ_text" : { 75 | "index" : 1, 76 | "type" : "sdf", 77 | "color" : [ 78 | 1.0, 79 | 1.0, 80 | 1.0, 81 | 1.0 82 | ], 83 | "alignX" : "center", 84 | "alignY" : "center", 85 | "value" : "Lorem\nIPSUM", 86 | "fontName" : "Georgia", 87 | "fontFile" : "fonts/Georgia.ttf", 88 | "size" : 1, 89 | "maxWidth" : 0, 90 | "overflow" : "NONE", 91 | "letterSpacing" : -0.10000002384185791, 92 | "lineSpacing" : 1.0000001192092896, 93 | "dimensions" : [ 94 | 2.3625001907348633, 95 | 1.69100022315979 96 | ] 97 | } 98 | }, 99 | "name" : "Text.001", 100 | "rotation" : [ 101 | 0.7071067690849304, 102 | 0, 103 | 0, 104 | 0.7071067690849304 105 | ], 106 | "translation" : [ 107 | 0, 108 | -1.3189244270324707, 109 | 0.09999999403953552 110 | ] 111 | }, 112 | { 113 | "extensions" : { 114 | "MOZ_text" : { 115 | "index" : 2, 116 | "type" : "sdf", 117 | "color" : [ 118 | 1.0, 119 | 1.0, 120 | 1.0, 121 | 1.0 122 | ], 123 | "alignX" : "center", 124 | "alignY" : "center", 125 | "value" : "Lorem\nIPSUM", 126 | "fontName" : "ArialMT", 127 | "fontFile" : "fonts/Arial.ttf", 128 | "size" : 1, 129 | "maxWidth" : 0, 130 | "overflow" : "NONE", 131 | "letterSpacing" : 1.3999998569488525, 132 | "lineSpacing" : 0.5, 133 | "dimensions" : [ 134 | 5.362500190734863, 135 | 1.190999984741211 136 | ] 137 | } 138 | }, 139 | "name" : "Text.002", 140 | "rotation" : [ 141 | 0.7071067690849304, 142 | 0, 143 | 0, 144 | 0.7071067690849304 145 | ], 146 | "translation" : [ 147 | 0, 148 | 1.905092477798462, 149 | 0.09999999403953552 150 | ] 151 | } 152 | ], 153 | "materials" : [ 154 | { 155 | "doubleSided" : true, 156 | "emissiveFactor" : [ 157 | 0, 158 | 0, 159 | 0 160 | ], 161 | "name" : "Material.004", 162 | "pbrMetallicRoughness" : { 163 | "baseColorFactor" : [ 164 | 0.09817364811897278, 165 | 0.529207170009613, 166 | 0.8000000715255737, 167 | 1 168 | ], 169 | "metallicFactor" : 0, 170 | "roughnessFactor" : 0.5 171 | } 172 | } 173 | ], 174 | "meshes" : [ 175 | { 176 | "name" : "Plane", 177 | "primitives" : [ 178 | { 179 | "attributes" : { 180 | "POSITION" : 0, 181 | "NORMAL" : 1, 182 | "TEXCOORD_0" : 2 183 | }, 184 | "indices" : 3, 185 | "material" : 0 186 | } 187 | ] 188 | } 189 | ], 190 | "accessors" : [ 191 | { 192 | "bufferView" : 0, 193 | "componentType" : 5126, 194 | "count" : 32, 195 | "max" : [ 196 | 1.8382400274276733, 197 | 1.040037602706434e-07, 198 | 3.601489782333374 199 | ], 200 | "min" : [ 201 | -1.8382400274276733, 202 | -8.061077494403435e-08, 203 | -3.4391820430755615 204 | ], 205 | "type" : "VEC3" 206 | }, 207 | { 208 | "bufferView" : 1, 209 | "componentType" : 5126, 210 | "count" : 32, 211 | "type" : "VEC3" 212 | }, 213 | { 214 | "bufferView" : 2, 215 | "componentType" : 5126, 216 | "count" : 32, 217 | "type" : "VEC2" 218 | }, 219 | { 220 | "bufferView" : 3, 221 | "componentType" : 5123, 222 | "count" : 48, 223 | "type" : "SCALAR" 224 | } 225 | ], 226 | "bufferViews" : [ 227 | { 228 | "buffer" : 0, 229 | "byteLength" : 384, 230 | "byteOffset" : 0 231 | }, 232 | { 233 | "buffer" : 0, 234 | "byteLength" : 384, 235 | "byteOffset" : 384 236 | }, 237 | { 238 | "buffer" : 0, 239 | "byteLength" : 256, 240 | "byteOffset" : 768 241 | }, 242 | { 243 | "buffer" : 0, 244 | "byteLength" : 96, 245 | "byteOffset" : 1024 246 | } 247 | ], 248 | "buffers" : [ 249 | { 250 | "byteLength" : 1120, 251 | "uri" : "data:application/octet-stream;base64,c0vrPz4crbMPbyTAc0vrP5VHlrOPG1zAcnSVP5VHlrOPG1zAcnSVPz4crbMPbyTAc0vrP6xY3zN61Q5Ac0vrPz4crbMPbyTAcnSVPz4crbMPbyTAcnSVP6xY3zN61Q5Ac0vrP++DpTPPfmZAc0vrP6xY3zN61Q5AcnSVP6xY3zN61Q5AcnSVP++DpTPPfmZAIMOYv++DpTPPfmZAIMOYv6xY3zN61Q5Ac0vrv6xY3zN61Q5Ac0vrv++DpTPPfmZAIMOYv6xY3zN61Q5AIMOYvz4crbMPbyTAc0vrvz4crbMPbyTAc0vrv6xY3zN61Q5AIMOYvz4crbMPbyTAIMOYv5VHlrOPG1zAc0vrv5VHlrOPG1zAc0vrvz4crbMPbyTAcnSVP++DpTPPfmZAcnSVP6xY3zN61Q5AIMOYv6xY3zN61Q5AIMOYv++DpTPPfmZAcnSVPz4crbMPbyTAcnSVP5VHlrOPG1zAIMOYv5VHlrOPG1zAIMOYvz4crbMPbyTAAAAAAP//fz/19VEyAAAAAP//fz/19VEyAAAAAP//fz/19VEyAAAAAP//fz/19VEyAAAAAAAAgD9lJyWzAAAAAAAAgD9lJyWzAAAAAAAAgD9lJyWzAAAAAAAAgD9lJyWzAAAAAP//fz+o4qgyAAAAAP//fz+o4qgyAAAAAP//fz+o4qgyAAAAAP//fz+o4qgyAAAAAP//fz+n4qgyAAAAAP//fz+n4qgyAAAAAP//fz+n4qgyAAAAAP//fz+n4qgyAAAAAP//fz9lJyWzAAAAAP//fz9lJyWzAAAAAP//fz9lJyWzAAAAAP//fz9lJyWzAAAAAAAAgD/09VEyAAAAAAAAgD/09VEyAAAAAAAAgD/09VEyAAAAAAAAgD/09VEyAAAAAAAAgD+o4qgyAAAAAAAAgD+o4qgyAAAAAAAAgD+o4qgyAAAAAAAAgD+o4qgyAAAAAAAAgD/19VEyAAAAAAAAgD/19VEyAAAAAAAAgD/19VEyAAAAAAAAgD/19VEyAACAP24Emz4AAIA/AAAAAN8faT8AAAAA3x9pP24Emz4AAIA/fFgCPwAAgD9uBJs+3x9pP24Emz7fH2k/fFgCPwAAgD8AAIA/AACAP3xYAj/fH2k/fFgCP98faT8AAIA/RJyoPQAAgD9EnKg9fFgCPwAAAAB8WAI/AAAAAAAAgD9EnKg9fFgCP0ScqD1uBJs+AAAAAG4Emz4AAAAAfFgCP0ScqD1uBJs+RJyoPQAAAAAAAAAAAAAAAAAAAABuBJs+3x9pPwAAgD/fH2k/fFgCP0ScqD18WAI/RJyoPQAAgD/fH2k/bgSbPt8faT8AAAAARJyoPQAAAABEnKg9bgSbPgAAAQACAAAAAgADAAQABQAGAAQABgAHAAgACQAKAAgACgALAAwADQAOAAwADgAPABAAEQASABAAEgATABQAFQAWABQAFgAXABgAGQAaABgAGgAbABwAHQAeABwAHgAfAA==" 252 | } 253 | ] 254 | } 255 | -------------------------------------------------------------------------------- /test/models/letterspacing.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4 19 | ] 20 | } 21 | ], 22 | "nodes" : [ 23 | { 24 | "extensions" : { 25 | "MOZ_text" : { 26 | "index" : 0, 27 | "type" : "sdf", 28 | "color" : [ 29 | 1.0, 30 | 1.0, 31 | 1.0, 32 | 1.0 33 | ], 34 | "alignX" : "left", 35 | "alignY" : "center", 36 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 37 | "fontName" : "ArialMT", 38 | "fontFile" : "fonts/Arial.ttf", 39 | "size" : 1, 40 | "maxWidth" : 0, 41 | "overflow" : "NONE", 42 | "letterSpacing" : 0, 43 | "lineSpacing" : 1, 44 | "dimensions" : [ 45 | 16.573999404907227, 46 | 0.9214074611663818 47 | ] 48 | } 49 | }, 50 | "name" : "Text", 51 | "rotation" : [ 52 | 0.7071067690849304, 53 | 0, 54 | 0, 55 | 0.7071068286895752 56 | ], 57 | "translation" : [ 58 | -7.698817253112793, 59 | 0.22439904510974884, 60 | 0.22439903020858765 61 | ] 62 | }, 63 | { 64 | "extensions" : { 65 | "MOZ_text" : { 66 | "index" : 1, 67 | "type" : "sdf", 68 | "color" : [ 69 | 1.0, 70 | 1.0, 71 | 1.0, 72 | 1.0 73 | ], 74 | "alignX" : "left", 75 | "alignY" : "center", 76 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 77 | "fontName" : "ArialMT", 78 | "fontFile" : "fonts/Arial.ttf", 79 | "size" : 1, 80 | "maxWidth" : 0, 81 | "overflow" : "NONE", 82 | "letterSpacing" : -0.5000000298023224, 83 | "lineSpacing" : 1, 84 | "dimensions" : [ 85 | 7.824000358581543, 86 | 0.9214074611663818 87 | ] 88 | } 89 | }, 90 | "name" : "Text.001", 91 | "rotation" : [ 92 | 0.7071067690849304, 93 | 0, 94 | 0, 95 | 0.7071068286895752 96 | ], 97 | "translation" : [ 98 | -7.698817253112793, 99 | 1.864822268486023, 100 | 0.22439903020858765 101 | ] 102 | }, 103 | { 104 | "extensions" : { 105 | "MOZ_text" : { 106 | "index" : 2, 107 | "type" : "sdf", 108 | "color" : [ 109 | 1.0, 110 | 1.0, 111 | 1.0, 112 | 1.0 113 | ], 114 | "alignX" : "left", 115 | "alignY" : "center", 116 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 117 | "fontName" : "ArialMT", 118 | "fontFile" : "fonts/Arial.ttf", 119 | "size" : 1, 120 | "maxWidth" : 0, 121 | "overflow" : "NONE", 122 | "letterSpacing" : -0.25, 123 | "lineSpacing" : 1, 124 | "dimensions" : [ 125 | 12.198999404907227, 126 | 0.9214074611663818 127 | ] 128 | } 129 | }, 130 | "name" : "Text.002", 131 | "rotation" : [ 132 | 0.7071067690849304, 133 | 0, 134 | 0, 135 | 0.7071068286895752 136 | ], 137 | "translation" : [ 138 | -7.698817253112793, 139 | 1.0656417608261108, 140 | 0.22439903020858765 141 | ] 142 | }, 143 | { 144 | "extensions" : { 145 | "MOZ_text" : { 146 | "index" : 3, 147 | "type" : "sdf", 148 | "color" : [ 149 | 1.0, 150 | 1.0, 151 | 1.0, 152 | 1.0 153 | ], 154 | "alignX" : "left", 155 | "alignY" : "center", 156 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 157 | "fontName" : "ArialMT", 158 | "fontFile" : "fonts/Arial.ttf", 159 | "size" : 1, 160 | "maxWidth" : 0, 161 | "overflow" : "NONE", 162 | "letterSpacing" : 0.25, 163 | "lineSpacing" : 1, 164 | "dimensions" : [ 165 | 20.948999404907227, 166 | 0.9214074611663818 167 | ] 168 | } 169 | }, 170 | "name" : "Text.003", 171 | "rotation" : [ 172 | 0.7071067690849304, 173 | 0, 174 | 0, 175 | 0.7071068286895752 176 | ], 177 | "translation" : [ 178 | -7.698817253112793, 179 | -0.5958125591278076, 180 | 0.22439903020858765 181 | ] 182 | }, 183 | { 184 | "extensions" : { 185 | "MOZ_text" : { 186 | "index" : 4, 187 | "type" : "sdf", 188 | "color" : [ 189 | 1.0, 190 | 1.0, 191 | 1.0, 192 | 1.0 193 | ], 194 | "alignX" : "left", 195 | "alignY" : "center", 196 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 197 | "fontName" : "ArialMT", 198 | "fontFile" : "fonts/Arial.ttf", 199 | "size" : 1, 200 | "maxWidth" : 0, 201 | "overflow" : "NONE", 202 | "letterSpacing" : 0.5, 203 | "lineSpacing" : 1, 204 | "dimensions" : [ 205 | 25.323999404907227, 206 | 0.9214074611663818 207 | ] 208 | } 209 | }, 210 | "name" : "Text.004", 211 | "rotation" : [ 212 | 0.7071067690849304, 213 | 0, 214 | 0, 215 | 0.7071068286895752 216 | ], 217 | "translation" : [ 218 | -7.698817253112793, 219 | -1.416024088859558, 220 | 0.2243988960981369 221 | ] 222 | } 223 | ] 224 | } 225 | -------------------------------------------------------------------------------- /test/models/lineheight.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4, 19 | 5 20 | ] 21 | } 22 | ], 23 | "nodes" : [ 24 | { 25 | "extensions" : { 26 | "MOZ_text" : { 27 | "index" : 0, 28 | "type" : "sdf", 29 | "color" : [ 30 | 1.0, 31 | 1.0, 32 | 1.0, 33 | 1.0 34 | ], 35 | "alignX" : "center", 36 | "alignY" : "top", 37 | "value" : "abc\ndef\nGHI\nJKL", 38 | "fontName" : "ArialMT", 39 | "fontFile" : "fonts/Arial.ttf", 40 | "size" : 1, 41 | "maxWidth" : 0, 42 | "overflow" : "NONE", 43 | "letterSpacing" : 0, 44 | "lineSpacing" : 0.75, 45 | "dimensions" : [ 46 | 1.5930001735687256, 47 | 3.1619999408721924 48 | ] 49 | } 50 | }, 51 | "name" : "Text", 52 | "rotation" : [ 53 | 0.7071067690849304, 54 | 0, 55 | 0, 56 | 0.7071068286895752 57 | ], 58 | "translation" : [ 59 | -1.9517333507537842, 60 | 2.6924829483032227, 61 | 0.0392022468149662 62 | ] 63 | }, 64 | { 65 | "extensions" : { 66 | "MOZ_text" : { 67 | "index" : 1, 68 | "type" : "sdf", 69 | "color" : [ 70 | 1.0, 71 | 1.0, 72 | 1.0, 73 | 1.0 74 | ], 75 | "alignX" : "center", 76 | "alignY" : "top", 77 | "value" : "abc\ndef\nGHI\nJKL", 78 | "fontName" : "ArialMT", 79 | "fontFile" : "fonts/Arial.ttf", 80 | "size" : 1, 81 | "maxWidth" : 0, 82 | "overflow" : "NONE", 83 | "letterSpacing" : 0, 84 | "lineSpacing" : 1, 85 | "dimensions" : [ 86 | 1.5930001735687256, 87 | 3.9119997024536133 88 | ] 89 | } 90 | }, 91 | "name" : "Text.001", 92 | "rotation" : [ 93 | 0.7071067690849304, 94 | 0, 95 | 0, 96 | 0.7071068286895752 97 | ], 98 | "translation" : [ 99 | 0.005494356155395508, 100 | 2.6924829483032227, 101 | 0.0392022468149662 102 | ] 103 | }, 104 | { 105 | "extensions" : { 106 | "MOZ_text" : { 107 | "index" : 2, 108 | "type" : "sdf", 109 | "color" : [ 110 | 1.0, 111 | 1.0, 112 | 1.0, 113 | 1.0 114 | ], 115 | "alignX" : "center", 116 | "alignY" : "top", 117 | "value" : "abc\ndef\nGHI\nJKL", 118 | "fontName" : "ArialMT", 119 | "fontFile" : "fonts/Arial.ttf", 120 | "size" : 1, 121 | "maxWidth" : 0, 122 | "overflow" : "NONE", 123 | "letterSpacing" : 0, 124 | "lineSpacing" : 0.5, 125 | "dimensions" : [ 126 | 1.5930001735687256, 127 | 2.4119999408721924 128 | ] 129 | } 130 | }, 131 | "name" : "Text.002", 132 | "rotation" : [ 133 | 0.7071067690849304, 134 | 0, 135 | 0, 136 | 0.7071068286895752 137 | ], 138 | "translation" : [ 139 | -3.9276013374328613, 140 | 2.6924829483032227, 141 | 0.0392022468149662 142 | ] 143 | }, 144 | { 145 | "extensions" : { 146 | "MOZ_text" : { 147 | "index" : 3, 148 | "type" : "sdf", 149 | "color" : [ 150 | 1.0, 151 | 1.0, 152 | 1.0, 153 | 1.0 154 | ], 155 | "alignX" : "center", 156 | "alignY" : "top", 157 | "value" : "abc\ndef\nGHI\nJKL", 158 | "fontName" : "ArialMT", 159 | "fontFile" : "fonts/Arial.ttf", 160 | "size" : 1, 161 | "maxWidth" : 0, 162 | "overflow" : "NONE", 163 | "letterSpacing" : 0, 164 | "lineSpacing" : 1.5, 165 | "dimensions" : [ 166 | 1.5930001735687256, 167 | 5.4120001792907715 168 | ] 169 | } 170 | }, 171 | "name" : "Text.003", 172 | "rotation" : [ 173 | 0.7071067690849304, 174 | 0, 175 | 0, 176 | 0.7071068286895752 177 | ], 178 | "translation" : [ 179 | 4.069072723388672, 180 | 2.6924829483032227, 181 | 0.0392022468149662 182 | ] 183 | }, 184 | { 185 | "extensions" : { 186 | "MOZ_text" : { 187 | "index" : 4, 188 | "type" : "sdf", 189 | "color" : [ 190 | 1.0, 191 | 1.0, 192 | 1.0, 193 | 1.0 194 | ], 195 | "alignX" : "center", 196 | "alignY" : "top", 197 | "value" : "abc\ndef\nGHI\nJKL", 198 | "fontName" : "ArialMT", 199 | "fontFile" : "fonts/Arial.ttf", 200 | "size" : 1, 201 | "maxWidth" : 0, 202 | "overflow" : "NONE", 203 | "letterSpacing" : 0, 204 | "lineSpacing" : 1.25, 205 | "dimensions" : [ 206 | 1.5930001735687256, 207 | 4.6620001792907715 208 | ] 209 | } 210 | }, 211 | "name" : "Text.004", 212 | "rotation" : [ 213 | 0.7071067690849304, 214 | 0, 215 | 0, 216 | 0.7071068286895752 217 | ], 218 | "translation" : [ 219 | 2.0372836589813232, 220 | 2.6924829483032227, 221 | 0.0392022468149662 222 | ] 223 | }, 224 | { 225 | "mesh" : 0, 226 | "name" : "Plane.001", 227 | "translation" : [ 228 | -5.6806206703186035, 229 | -0.186402827501297, 230 | 0.007173687219619751 231 | ] 232 | } 233 | ], 234 | "materials" : [ 235 | { 236 | "doubleSided" : true, 237 | "emissiveFactor" : [ 238 | 0, 239 | 0, 240 | 0 241 | ], 242 | "name" : "Material.002", 243 | "pbrMetallicRoughness" : { 244 | "baseColorFactor" : [ 245 | 0.4819340705871582, 246 | 0.020676877349615097, 247 | 0.11108145862817764, 248 | 1 249 | ], 250 | "metallicFactor" : 0, 251 | "roughnessFactor" : 0.5 252 | } 253 | } 254 | ], 255 | "meshes" : [ 256 | { 257 | "name" : "Plane.003", 258 | "primitives" : [ 259 | { 260 | "attributes" : { 261 | "POSITION" : 0, 262 | "NORMAL" : 1, 263 | "TEXCOORD_0" : 2 264 | }, 265 | "indices" : 3, 266 | "material" : 0 267 | } 268 | ] 269 | } 270 | ], 271 | "accessors" : [ 272 | { 273 | "bufferView" : 0, 274 | "componentType" : 5126, 275 | "count" : 36, 276 | "max" : [ 277 | 11.320137023925781, 278 | 3.4544122219085693, 279 | 3.5762786865234375e-07 280 | ], 281 | "min" : [ 282 | 0.2135324478149414, 283 | -3.0401837825775146, 284 | -3.5762786865234375e-07 285 | ], 286 | "type" : "VEC3" 287 | }, 288 | { 289 | "bufferView" : 1, 290 | "componentType" : 5126, 291 | "count" : 36, 292 | "type" : "VEC3" 293 | }, 294 | { 295 | "bufferView" : 2, 296 | "componentType" : 5126, 297 | "count" : 36, 298 | "type" : "VEC2" 299 | }, 300 | { 301 | "bufferView" : 3, 302 | "componentType" : 5123, 303 | "count" : 48, 304 | "type" : "SCALAR" 305 | } 306 | ], 307 | "bufferViews" : [ 308 | { 309 | "buffer" : 0, 310 | "byteLength" : 432, 311 | "byteOffset" : 0 312 | }, 313 | { 314 | "buffer" : 0, 315 | "byteLength" : 432, 316 | "byteOffset" : 432 317 | }, 318 | { 319 | "buffer" : 0, 320 | "byteLength" : 288, 321 | "byteOffset" : 864 322 | }, 323 | { 324 | "buffer" : 0, 325 | "byteLength" : 96, 326 | "byteOffset" : 1152 327 | } 328 | ], 329 | "buffers" : [ 330 | { 331 | "byteLength" : 1248, 332 | "uri" : "data:application/octet-stream;base64,weERQbrxFsAAAAA0QKhaPl+SQsDx71k0SB81QV+SQsDx71k0jmAmQbnxFsAAAMA0SB81QRcVXUCD9g20jmAmQXN0MUAAAMC0weERQbrxFsAAAAA0weERQTBOyL/IMm8zQKhaPl+SQsDx71k0QKhaPl+SQsDx71k0rpviQNwFUr+gDU0zVJehQNYFUr/AGjmyQKhaPl+SQsDx71k01MKhQAhdlr0ckmcyd+VEQB8pir2lhqyzQKhaPhcVXUCD9g20QKhaPl+SQsDx71k01UqRP+xJLj/KEL2y1UqRP3J0MUAAAAC0jmAmQXN0MUAAAMC0SB81QRcVXUCD9g20QKhaPl+SQsDx71k0weERQTBOyL/IMm8znn/iQHwyyL83OgkzQKhaPl+SQsDx71k0nn/iQHwyyL83OgkzrpviQNwFUr+gDU0zQKhaPl+SQsDx71k0VJehQNYFUr/AGjmy1MKhQAhdlr0ckmcyQKhaPl+SQsDx71k0cvhEQIe1Lj+gwj+x1UqRP+xJLj/KEL2ycvhEQIe1Lj+gwj+xQKhaPl+SQsDx71k0d+VEQB8pir2lhqyzcqQAswf0pTMAAIA/cqQAswf0pTMAAIA/cqQAswf0pTMAAIA/cqQAswf0pTMAAIA/cqQAswf0pTMAAIA/cqQAswf0pTMAAIA/a2FhMUjfqzP//38/a2FhMUjfqzP//38/a2FhMUjfqzP//38/12X3sjLEKjQAAIA/12X3sjLEKjQAAIA/12X3sjLEKjQAAIA/O9dJs4LtFjQAAIA/O9dJs4LtFjQAAIA/O9dJs4LtFjQAAIA/l7yjMtverbMAAIA/l7yjMtverbMAAIA/l7yjMtverbMAAIA/l7yjMtverbMAAIA/l7yjMtverbMAAIA/l7yjMtverbMAAIA/usdGsl8sNjQAAIA/usdGsl8sNjQAAIA/usdGsl8sNjQAAIA/Z3v9Mm6Vt7IAAIA/Z3v9Mm6Vt7IAAIA/Z3v9Mm6Vt7IAAIA/vql+M2RDDbMAAIA/vql+M2RDDbMAAIA/vql+M2RDDbMAAIA/7+MqstYhhzP//38/7+MqstYhhzP//38/7+MqstYhhzP//38/YOZdNBPB3rMAAIA/YOZdNBPB3rMAAIA/YOZdNBPB3rMAAIA/JVcgPlUfTD8AAAAAAACAPwAAgD8AAIA/NupXP1UfTD8AAIA/AAAAADbqVz+ogk8+JVcgPlUfTD8lVyA+u580PwAAAAAAAIA/AAAAAJN8Hj8lVyA+knwePyVXID6G4hQ/AAAAAGNYCD8lVyA+d5sIPyVXID4AUvE+AAAAAAAAAAAAAAAA8uPjPiVXID7w4+M+JVcgPqyCTz426lc/qIJPPgAAgD8AAAAAAAAAALyfND8lVyA+u580PyVXID6aKic/AAAAADJhKz8lVyA+mionPyVXID6SfB4/AAAAAIbiFD8lVyA+huIUPyVXID53mwg/AAAAAAsIAD8lVyA+ECDqPiVXID7w4+M+JVcgPhAg6j4AAAAACwgAPyVXID4AUvE+AAABAAIAAAACAAMAAwACAAQABQADAAQABgAHAAgACQAKAAsADAANAA4ADwAQABEADwARABIADwASABMADwATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMA" 333 | } 334 | ] 335 | } 336 | -------------------------------------------------------------------------------- /test/models/lorem ipsun.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0 15 | ] 16 | } 17 | ], 18 | "nodes" : [ 19 | { 20 | "extensions" : { 21 | "MOZ_text" : { 22 | "index" : 0, 23 | "type" : "sdf", 24 | "color" : [ 25 | 1.0, 26 | 0.08084166049957275, 27 | 0.03721405938267708, 28 | 1.0 29 | ], 30 | "alignX" : "center", 31 | "alignY" : "center", 32 | "value" : "Lorem\nIpsun", 33 | "fontName" : "Impact", 34 | "fontFile" : "fonts/Impact.ttf", 35 | "size" : 1, 36 | "maxWidth" : 0, 37 | "overflow" : "NONE", 38 | "letterSpacing" : 0, 39 | "lineSpacing" : 1, 40 | "dimensions" : [ 41 | 1.6402738094329834, 42 | 1.5891170501708984 43 | ] 44 | } 45 | }, 46 | "name" : "Text", 47 | "rotation" : [ 48 | 0.7071067690849304, 49 | 0, 50 | 0, 51 | 0.7071068286895752 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /test/models/materials.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4, 19 | 5, 20 | 6 21 | ] 22 | } 23 | ], 24 | "nodes" : [ 25 | { 26 | "extensions" : { 27 | "MOZ_text" : { 28 | "index" : 0, 29 | "type" : "sdf", 30 | "color" : [ 31 | 1.0, 32 | 0.005511130206286907, 33 | 0.010657832026481628, 34 | 1.0 35 | ], 36 | "alignX" : "left", 37 | "alignY" : "center", 38 | "value" : "COLOR", 39 | "fontName" : "Impact", 40 | "fontFile" : "fonts/Impact.ttf", 41 | "size" : 1, 42 | "maxWidth" : 0, 43 | "overflow" : "NONE", 44 | "letterSpacing" : 0, 45 | "lineSpacing" : 1, 46 | "dimensions" : [ 47 | 3.3999998569488525, 48 | 0.7000000476837158 49 | ] 50 | } 51 | }, 52 | "name" : "Text.001", 53 | "rotation" : [ 54 | 0.7071067690849304, 55 | 0, 56 | 0, 57 | 0.7071068286895752 58 | ], 59 | "translation" : [ 60 | -2.0110862255096436, 61 | 0.763002336025238, 62 | 0 63 | ] 64 | }, 65 | { 66 | "extensions" : { 67 | "MOZ_text" : { 68 | "index" : 1, 69 | "type" : "sdf", 70 | "color" : [ 71 | 1.0, 72 | 0.2460407316684723, 73 | 0.007256365846842527, 74 | 1.0 75 | ], 76 | "alignX" : "left", 77 | "alignY" : "center", 78 | "value" : "COLOR", 79 | "fontName" : "Impact", 80 | "fontFile" : "fonts/Impact.ttf", 81 | "size" : 1, 82 | "maxWidth" : 0, 83 | "overflow" : "NONE", 84 | "letterSpacing" : 0, 85 | "lineSpacing" : 1, 86 | "dimensions" : [ 87 | 3.3999998569488525, 88 | 0.7000000476837158 89 | ] 90 | } 91 | }, 92 | "name" : "Text.002", 93 | "rotation" : [ 94 | 0.7071067690849304, 95 | 0, 96 | 0, 97 | 0.7071068286895752 98 | ], 99 | "translation" : [ 100 | -2.0110862255096436, 101 | 0.0032998323440551758, 102 | 0 103 | ] 104 | }, 105 | { 106 | "extensions" : { 107 | "MOZ_text" : { 108 | "index" : 2, 109 | "type" : "sdf", 110 | "color" : [ 111 | 1.0, 112 | 0.6041886210441589, 113 | 0.0, 114 | 1.0 115 | ], 116 | "alignX" : "left", 117 | "alignY" : "center", 118 | "value" : "COLOR", 119 | "fontName" : "Impact", 120 | "fontFile" : "fonts/Impact.ttf", 121 | "size" : 1, 122 | "maxWidth" : 0, 123 | "overflow" : "NONE", 124 | "letterSpacing" : 0, 125 | "lineSpacing" : 1, 126 | "dimensions" : [ 127 | 3.3999998569488525, 128 | 0.7000000476837158 129 | ] 130 | } 131 | }, 132 | "name" : "Text.003", 133 | "rotation" : [ 134 | 0.7071067690849304, 135 | 0, 136 | 0, 137 | 0.7071068286895752 138 | ], 139 | "translation" : [ 140 | -2.0110862255096436, 141 | -0.7949102520942688, 142 | 0 143 | ] 144 | }, 145 | { 146 | "extensions" : { 147 | "MOZ_text" : { 148 | "index" : 3, 149 | "type" : "sdf", 150 | "color" : [ 151 | 0.004522898234426975, 152 | 1.0, 153 | 0.011849410831928253, 154 | 1.0 155 | ], 156 | "alignX" : "left", 157 | "alignY" : "center", 158 | "value" : "COLOR", 159 | "fontName" : "Impact", 160 | "fontFile" : "fonts/Impact.ttf", 161 | "size" : 1, 162 | "maxWidth" : 0, 163 | "overflow" : "NONE", 164 | "letterSpacing" : 0, 165 | "lineSpacing" : 1, 166 | "dimensions" : [ 167 | 3.3999998569488525, 168 | 0.7000000476837158 169 | ] 170 | } 171 | }, 172 | "name" : "Text.004", 173 | "rotation" : [ 174 | 0.7071067690849304, 175 | 0, 176 | 0, 177 | 0.7071068286895752 178 | ], 179 | "translation" : [ 180 | 0.3059403598308563, 181 | 0.763002336025238, 182 | 0 183 | ] 184 | }, 185 | { 186 | "extensions" : { 187 | "MOZ_text" : { 188 | "index" : 4, 189 | "type" : "sdf", 190 | "color" : [ 191 | 0.0020003956742584705, 192 | 0.915272057056427, 193 | 1.0, 194 | 1.0 195 | ], 196 | "alignX" : "left", 197 | "alignY" : "center", 198 | "value" : "COLOR", 199 | "fontName" : "Impact", 200 | "fontFile" : "fonts/Impact.ttf", 201 | "size" : 1, 202 | "maxWidth" : 0, 203 | "overflow" : "NONE", 204 | "letterSpacing" : 0, 205 | "lineSpacing" : 1, 206 | "dimensions" : [ 207 | 3.3999998569488525, 208 | 0.7000000476837158 209 | ] 210 | } 211 | }, 212 | "name" : "Text.005", 213 | "rotation" : [ 214 | 0.7071067690849304, 215 | 0, 216 | 0, 217 | 0.7071068286895752 218 | ], 219 | "translation" : [ 220 | 0.3059403598308563, 221 | 0.0032998323440551758, 222 | 0 223 | ] 224 | }, 225 | { 226 | "extensions" : { 227 | "MOZ_text" : { 228 | "index" : 5, 229 | "type" : "sdf", 230 | "color" : [ 231 | 0.017666403204202652, 232 | 0.0, 233 | 1.0, 234 | 1.0 235 | ], 236 | "alignX" : "left", 237 | "alignY" : "center", 238 | "value" : "COLOR", 239 | "fontName" : "Impact", 240 | "fontFile" : "fonts/Impact.ttf", 241 | "size" : 1, 242 | "maxWidth" : 0, 243 | "overflow" : "NONE", 244 | "letterSpacing" : 0, 245 | "lineSpacing" : 1, 246 | "dimensions" : [ 247 | 3.3999998569488525, 248 | 0.7000000476837158 249 | ] 250 | } 251 | }, 252 | "name" : "Text.006", 253 | "rotation" : [ 254 | 0.7071067690849304, 255 | 0, 256 | 0, 257 | 0.7071068286895752 258 | ], 259 | "translation" : [ 260 | 0.3059403598308563, 261 | -0.7949102520942688, 262 | 0 263 | ] 264 | }, 265 | { 266 | "extensions" : { 267 | "MOZ_text" : { 268 | "index" : 6, 269 | "type" : "sdf", 270 | "color" : [ 271 | 1.0, 272 | 0.03717641904950142, 273 | 0.25428420305252075, 274 | 0.4928571581840515 275 | ], 276 | "alignX" : "center", 277 | "alignY" : "center", 278 | "value" : "TRANS\nPARENT", 279 | "fontName" : "ArialMT", 280 | "fontFile" : "fonts/Arial.ttf", 281 | "size" : 1, 282 | "maxWidth" : 0, 283 | "overflow" : "NONE", 284 | "letterSpacing" : 0, 285 | "lineSpacing" : 0.6000000238418579, 286 | "dimensions" : [ 287 | 6.370627403259277, 288 | 2.297982692718506 289 | ] 290 | } 291 | }, 292 | "name" : "Text.007", 293 | "rotation" : [ 294 | 0.7071067690849304, 295 | 0, 296 | 0, 297 | 0.7071068286895752 298 | ], 299 | "scale" : [ 300 | 1.7800019979476929, 301 | 1.7800019979476929, 302 | 1.7800019979476929 303 | ], 304 | "translation" : [ 305 | 0, 306 | 0, 307 | 0.11286976933479309 308 | ] 309 | } 310 | ] 311 | } 312 | -------------------------------------------------------------------------------- /test/models/overflow.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text", 8 | "KHR_materials_unlit" 9 | ], 10 | "scene" : 0, 11 | "scenes" : [ 12 | { 13 | "name" : "Scene", 14 | "nodes" : [ 15 | 0, 16 | 1, 17 | 2, 18 | 3, 19 | 4 20 | ] 21 | } 22 | ], 23 | "nodes" : [ 24 | { 25 | "extensions" : { 26 | "MOZ_text" : { 27 | "index" : 0, 28 | "type" : "sdf", 29 | "color" : [ 30 | 1.0, 31 | 1.0, 32 | 1.0, 33 | 1.0 34 | ], 35 | "alignX" : "left", 36 | "alignY" : "center", 37 | "value" : "No wrap:\nTo be, or not to be, that is the question", 38 | "fontName" : "ArialMT", 39 | "fontFile" : "fonts/Arial.ttf", 40 | "size" : 1, 41 | "maxWidth" : 0, 42 | "overflow" : "NONE", 43 | "letterSpacing" : 0, 44 | "lineSpacing" : 1, 45 | "dimensions" : [ 46 | 16.171001434326172, 47 | 1.9119999408721924 48 | ] 49 | } 50 | }, 51 | "name" : "Text", 52 | "rotation" : [ 53 | 0.7071067690849304, 54 | 0, 55 | 0, 56 | 0.7071068286895752 57 | ], 58 | "translation" : [ 59 | -7.937744140625, 60 | 4.8026323318481445, 61 | 0 62 | ] 63 | }, 64 | { 65 | "extensions" : { 66 | "MOZ_text" : { 67 | "index" : 1, 68 | "type" : "sdf", 69 | "color" : [ 70 | 1.0, 71 | 1.0, 72 | 1.0, 73 | 1.0 74 | ], 75 | "alignX" : "left", 76 | "alignY" : "top", 77 | "value" : "Overflow:\nTo be, or not to be, that is the question", 78 | "fontName" : "ArialMT", 79 | "fontFile" : "fonts/Arial.ttf", 80 | "size" : 1, 81 | "maxWidth" : 5, 82 | "overflow" : "NONE", 83 | "letterSpacing" : 0, 84 | "lineSpacing" : 1, 85 | "dimensions" : [ 86 | 4.0450005531311035, 87 | 4.921407222747803 88 | ] 89 | } 90 | }, 91 | "name" : "Text.001", 92 | "rotation" : [ 93 | 0.7071067690849304, 94 | 0, 95 | 0, 96 | 0.7071068286895752 97 | ], 98 | "translation" : [ 99 | -8.024392127990723, 100 | 2.1087992191314697, 101 | 0 102 | ] 103 | }, 104 | { 105 | "extensions" : { 106 | "MOZ_text" : { 107 | "index" : 2, 108 | "type" : "sdf", 109 | "color" : [ 110 | 1.0, 111 | 1.0, 112 | 1.0, 113 | 1.0 114 | ], 115 | "alignX" : "left", 116 | "alignY" : "top", 117 | "value" : "Scale to Fit:\nTo be, or not to be, that is the question", 118 | "fontName" : "ArialMT", 119 | "fontFile" : "fonts/Arial.ttf", 120 | "size" : 1, 121 | "maxWidth" : 5, 122 | "overflow" : "SCALE", 123 | "letterSpacing" : 0, 124 | "lineSpacing" : 1, 125 | "dimensions" : [ 126 | 4.975997447967529, 127 | 0.5911130905151367 128 | ] 129 | } 130 | }, 131 | "name" : "Text.002", 132 | "rotation" : [ 133 | 0.7071067690849304, 134 | 0, 135 | 0, 136 | 0.7071068286895752 137 | ], 138 | "translation" : [ 139 | -2.0540249347686768, 140 | 2.1087992191314697, 141 | 0 142 | ] 143 | }, 144 | { 145 | "extensions" : { 146 | "MOZ_text" : { 147 | "index" : 3, 148 | "type" : "sdf", 149 | "color" : [ 150 | 1.0, 151 | 1.0, 152 | 1.0, 153 | 1.0 154 | ], 155 | "alignX" : "left", 156 | "alignY" : "top", 157 | "value" : "Truncate:\nTo be, or not to be, that is the question", 158 | "fontName" : "ArialMT", 159 | "fontFile" : "fonts/Arial.ttf", 160 | "size" : 1, 161 | "maxWidth" : 5, 162 | "overflow" : "TRUNCATE", 163 | "letterSpacing" : 0, 164 | "lineSpacing" : 1, 165 | "dimensions" : [ 166 | 3.9640004634857178, 167 | 2.799999952316284 168 | ] 169 | } 170 | }, 171 | "name" : "Text.003", 172 | "rotation" : [ 173 | 0.7071067690849304, 174 | 0, 175 | 0, 176 | 0.7071068286895752 177 | ], 178 | "translation" : [ 179 | 3.6424720287323, 180 | 2.1087992191314697, 181 | 0 182 | ] 183 | }, 184 | { 185 | "mesh" : 0, 186 | "name" : "Plane", 187 | "rotation" : [ 188 | 0.7071067690849304, 189 | 0, 190 | 0, 191 | 0.7071068286895752 192 | ], 193 | "translation" : [ 194 | 0, 195 | 3.532923460006714, 196 | 0 197 | ] 198 | } 199 | ], 200 | "materials" : [ 201 | { 202 | "doubleSided" : true, 203 | "extensions" : { 204 | "KHR_materials_unlit" : {} 205 | }, 206 | "name" : "Material.002", 207 | "pbrMetallicRoughness" : { 208 | "baseColorFactor" : [ 209 | 0.18731386959552765, 210 | 0.18731386959552765, 211 | 0.18731386959552765, 212 | 1 213 | ] 214 | } 215 | } 216 | ], 217 | "meshes" : [ 218 | { 219 | "name" : "Plane", 220 | "primitives" : [ 221 | { 222 | "attributes" : { 223 | "POSITION" : 0, 224 | "NORMAL" : 1, 225 | "TEXCOORD_0" : 2 226 | }, 227 | "indices" : 3, 228 | "material" : 0 229 | } 230 | ] 231 | } 232 | ], 233 | "accessors" : [ 234 | { 235 | "bufferView" : 0, 236 | "componentType" : 5126, 237 | "count" : 4, 238 | "max" : [ 239 | 7.998596668243408, 240 | 5.67506390325434e-07, 241 | 0.4817476272583008 242 | ], 243 | "min" : [ 244 | -7.998596668243408, 245 | -5.67506390325434e-07, 246 | -0.4817471504211426 247 | ], 248 | "type" : "VEC3" 249 | }, 250 | { 251 | "bufferView" : 1, 252 | "componentType" : 5126, 253 | "count" : 4, 254 | "type" : "VEC3" 255 | }, 256 | { 257 | "bufferView" : 2, 258 | "componentType" : 5126, 259 | "count" : 4, 260 | "type" : "VEC2" 261 | }, 262 | { 263 | "bufferView" : 3, 264 | "componentType" : 5123, 265 | "count" : 6, 266 | "type" : "SCALAR" 267 | } 268 | ], 269 | "bufferViews" : [ 270 | { 271 | "buffer" : 0, 272 | "byteLength" : 48, 273 | "byteOffset" : 0 274 | }, 275 | { 276 | "buffer" : 0, 277 | "byteLength" : 48, 278 | "byteOffset" : 48 279 | }, 280 | { 281 | "buffer" : 0, 282 | "byteLength" : 32, 283 | "byteOffset" : 96 284 | }, 285 | { 286 | "buffer" : 0, 287 | "byteLength" : 12, 288 | "byteOffset" : 128 289 | } 290 | ], 291 | "buffers" : [ 292 | { 293 | "byteLength" : 140, 294 | "uri" : "data:application/octet-stream;base64,gfT/QL5WGDWgp/Y+gfT/QL5WGLWQp/a+gfT/wL5WGLWQp/a+gfT/wL5WGDWgp/Y+AAAAAAAAgD9XHJ61AAAAAAAAgD9XHJ61AAAAAAAAgD9XHJ61AAAAAAAAgD9XHJ61AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAEAAgAAAAIAAwA=" 295 | } 296 | ] 297 | } 298 | -------------------------------------------------------------------------------- /test/models/sizes.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4 19 | ] 20 | } 21 | ], 22 | "nodes" : [ 23 | { 24 | "extensions" : { 25 | "MOZ_text" : { 26 | "index" : 0, 27 | "type" : "sdf", 28 | "color" : [ 29 | 1.0, 30 | 1.0, 31 | 1.0, 32 | 1.0 33 | ], 34 | "alignX" : "left", 35 | "alignY" : "center", 36 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 37 | "fontName" : "ArialMT", 38 | "fontFile" : "fonts/Arial.ttf", 39 | "size" : 1, 40 | "maxWidth" : 0, 41 | "overflow" : "NONE", 42 | "letterSpacing" : 0, 43 | "lineSpacing" : 1, 44 | "dimensions" : [ 45 | 16.573999404907227, 46 | 0.9214074611663818 47 | ] 48 | } 49 | }, 50 | "name" : "Text", 51 | "rotation" : [ 52 | 0.7071067690849304, 53 | 0, 54 | 0, 55 | 0.7071068286895752 56 | ], 57 | "translation" : [ 58 | -8.981712341308594, 59 | 0.5188339948654175, 60 | 0.22439903020858765 61 | ] 62 | }, 63 | { 64 | "extensions" : { 65 | "MOZ_text" : { 66 | "index" : 1, 67 | "type" : "sdf", 68 | "color" : [ 69 | 1.0, 70 | 1.0, 71 | 1.0, 72 | 1.0 73 | ], 74 | "alignX" : "left", 75 | "alignY" : "center", 76 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 77 | "fontName" : "ArialMT", 78 | "fontFile" : "fonts/Arial.ttf", 79 | "size" : 0.5, 80 | "maxWidth" : 0, 81 | "overflow" : "NONE", 82 | "letterSpacing" : 0, 83 | "lineSpacing" : 1, 84 | "dimensions" : [ 85 | 8.286999702453613, 86 | 0.4607037305831909 87 | ] 88 | } 89 | }, 90 | "name" : "Text.001", 91 | "rotation" : [ 92 | 0.7071067690849304, 93 | 0, 94 | 0, 95 | 0.7071068286895752 96 | ], 97 | "translation" : [ 98 | -8.981712341308594, 99 | 1.974169135093689, 100 | 0.22439885139465332 101 | ] 102 | }, 103 | { 104 | "extensions" : { 105 | "MOZ_text" : { 106 | "index" : 2, 107 | "type" : "sdf", 108 | "color" : [ 109 | 1.0, 110 | 1.0, 111 | 1.0, 112 | 1.0 113 | ], 114 | "alignX" : "left", 115 | "alignY" : "center", 116 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 117 | "fontName" : "ArialMT", 118 | "fontFile" : "fonts/Arial.ttf", 119 | "size" : 0.75, 120 | "maxWidth" : 0, 121 | "overflow" : "NONE", 122 | "letterSpacing" : 0, 123 | "lineSpacing" : 1, 124 | "dimensions" : [ 125 | 12.430500030517578, 126 | 0.6910556554794312 127 | ] 128 | } 129 | }, 130 | "name" : "Text.002", 131 | "rotation" : [ 132 | 0.7071067690849304, 133 | 0, 134 | 0, 135 | 0.7071068286895752 136 | ], 137 | "translation" : [ 138 | -8.981712341308594, 139 | 1.3232488632202148, 140 | 0.2243989109992981 141 | ] 142 | }, 143 | { 144 | "extensions" : { 145 | "MOZ_text" : { 146 | "index" : 3, 147 | "type" : "sdf", 148 | "color" : [ 149 | 1.0, 150 | 1.0, 151 | 1.0, 152 | 1.0 153 | ], 154 | "alignX" : "left", 155 | "alignY" : "center", 156 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 157 | "fontName" : "ArialMT", 158 | "fontFile" : "fonts/Arial.ttf", 159 | "size" : 1.25, 160 | "maxWidth" : 0, 161 | "overflow" : "NONE", 162 | "letterSpacing" : 0, 163 | "lineSpacing" : 1, 164 | "dimensions" : [ 165 | 20.717500686645508, 166 | 1.151759386062622 167 | ] 168 | } 169 | }, 170 | "name" : "Text.003", 171 | "rotation" : [ 172 | 0.7071067690849304, 173 | 0, 174 | 0, 175 | 0.7071068286895752 176 | ], 177 | "translation" : [ 178 | -8.981712341308594, 179 | -0.38100188970565796, 180 | 0.22439908981323242 181 | ] 182 | }, 183 | { 184 | "extensions" : { 185 | "MOZ_text" : { 186 | "index" : 4, 187 | "type" : "sdf", 188 | "color" : [ 189 | 1.0, 190 | 1.0, 191 | 1.0, 192 | 1.0 193 | ], 194 | "alignX" : "left", 195 | "alignY" : "center", 196 | "value" : "0123456789abcdefghijklmnopqrstuvwxyz", 197 | "fontName" : "ArialMT", 198 | "fontFile" : "fonts/Arial.ttf", 199 | "size" : 1.5, 200 | "maxWidth" : 0, 201 | "overflow" : "NONE", 202 | "letterSpacing" : 0, 203 | "lineSpacing" : 1, 204 | "dimensions" : [ 205 | 24.861000061035156, 206 | 1.3821113109588623 207 | ] 208 | } 209 | }, 210 | "name" : "Text.004", 211 | "rotation" : [ 212 | 0.7071067690849304, 213 | 0, 214 | 0, 215 | 0.7071068286895752 216 | ], 217 | "translation" : [ 218 | -8.981712341308594, 219 | -1.4793606996536255, 220 | 0.22439932823181152 221 | ] 222 | } 223 | ] 224 | } 225 | -------------------------------------------------------------------------------- /test/models/types.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.1.45", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "MOZ_text" 8 | ], 9 | "scene" : 0, 10 | "scenes" : [ 11 | { 12 | "name" : "Scene", 13 | "nodes" : [ 14 | 0, 15 | 1, 16 | 2, 17 | 3 18 | ] 19 | } 20 | ], 21 | "nodes" : [ 22 | { 23 | "extensions" : { 24 | "MOZ_text" : { 25 | "index" : 0, 26 | "type" : "sdf", 27 | "color" : [ 28 | 1.0, 29 | 1.0, 30 | 1.0, 31 | 1.0 32 | ], 33 | "alignX" : "center", 34 | "alignY" : "center", 35 | "value" : "SDF \nthis is a test", 36 | "fontName" : "Impact", 37 | "fontFile" : "fonts/Impact.ttf", 38 | "size" : 2, 39 | "maxWidth" : 0, 40 | "overflow" : "NONE", 41 | "letterSpacing" : 0, 42 | "lineSpacing" : 0.5, 43 | "dimensions" : [ 44 | 9.66200065612793, 45 | 2.4000000953674316 46 | ] 47 | } 48 | }, 49 | "name" : "Text", 50 | "rotation" : [ 51 | 0.7071067690849304, 52 | 0, 53 | 0, 54 | 0.7071068286895752 55 | ], 56 | "translation" : [ 57 | 0, 58 | 4.204799652099609, 59 | 0 60 | ] 61 | }, 62 | { 63 | "extensions" : { 64 | "MOZ_text" : { 65 | "index" : 1, 66 | "type" : "geometry", 67 | "color" : [ 68 | 1.0, 69 | 0.986351490020752, 70 | 1.0, 71 | 1.0 72 | ], 73 | "alignX" : "left", 74 | "alignY" : "top", 75 | "value" : "Geometry \nthis is a test", 76 | "fontName" : "Impact", 77 | "fontFile" : "fonts/Impact.ttf", 78 | "size" : 2, 79 | "maxWidth" : 0, 80 | "overflow" : "NONE", 81 | "letterSpacing" : 0, 82 | "lineSpacing" : 0.5600000023841858, 83 | "dimensions" : [ 84 | 9.702001571655273, 85 | 2.5611627101898193 86 | ], 87 | "extrude" : 0.6399999260902405, 88 | "extrudeResolution" : 2, 89 | "bevel" : 0.03999999165534973, 90 | "bevelOffset" : -0.019999980926513672, 91 | "bevelResolution" : 1 92 | } 93 | }, 94 | "name" : "Text.002", 95 | "rotation" : [ 96 | 0.7071067690849304, 97 | 0, 98 | 0, 99 | 0.7071068286895752 100 | ], 101 | "translation" : [ 102 | -3.314697742462158, 103 | -0.32887446880340576, 104 | 0 105 | ] 106 | }, 107 | { 108 | "extensions" : { 109 | "MOZ_text" : { 110 | "index" : 2, 111 | "type" : "texture", 112 | "color" : [ 113 | 1.0, 114 | 1.0, 115 | 1.0, 116 | 1.0 117 | ], 118 | "alignX" : "center", 119 | "alignY" : "center", 120 | "value" : "Texture \nthis is a test", 121 | "fontName" : "Impact", 122 | "fontFile" : "fonts/Impact.ttf", 123 | "size" : 2, 124 | "maxWidth" : 0, 125 | "overflow" : "NONE", 126 | "letterSpacing" : 0, 127 | "lineSpacing" : 0.5, 128 | "dimensions" : [ 129 | 9.66200065612793, 130 | 2.381999969482422 131 | ] 132 | } 133 | }, 134 | "name" : "Text.001", 135 | "rotation" : [ 136 | 0.7071067690849304, 137 | 0, 138 | 0, 139 | 0.7071068286895752 140 | ], 141 | "translation" : [ 142 | 0, 143 | 1.3620043992996216, 144 | 0.051436007022857666 145 | ] 146 | }, 147 | { 148 | "mesh" : 0, 149 | "name" : "Plane", 150 | "translation" : [ 151 | 0, 152 | 0, 153 | -0.12198308855295181 154 | ] 155 | } 156 | ], 157 | "materials" : [ 158 | { 159 | "doubleSided" : true, 160 | "emissiveFactor" : [ 161 | 0, 162 | 0, 163 | 0 164 | ], 165 | "name" : "Material.002", 166 | "pbrMetallicRoughness" : { 167 | "baseColorFactor" : [ 168 | 0.03484096750617027, 169 | 0.1259651929140091, 170 | 0.010935947299003601, 171 | 1 172 | ], 173 | "metallicFactor" : 0, 174 | "roughnessFactor" : 0.5 175 | } 176 | } 177 | ], 178 | "meshes" : [ 179 | { 180 | "name" : "Plane", 181 | "primitives" : [ 182 | { 183 | "attributes" : { 184 | "POSITION" : 0, 185 | "NORMAL" : 1, 186 | "TEXCOORD_0" : 2 187 | }, 188 | "indices" : 3, 189 | "material" : 0 190 | } 191 | ] 192 | } 193 | ], 194 | "accessors" : [ 195 | { 196 | "bufferView" : 0, 197 | "componentType" : 5126, 198 | "count" : 70, 199 | "max" : [ 200 | 4.996329307556152, 201 | 6.826893329620361, 202 | 0.010078459978103638 203 | ], 204 | "min" : [ 205 | -5.157411575317383, 206 | -4.638166904449463, 207 | 0.010077983140945435 208 | ], 209 | "type" : "VEC3" 210 | }, 211 | { 212 | "bufferView" : 1, 213 | "componentType" : 5126, 214 | "count" : 70, 215 | "type" : "VEC3" 216 | }, 217 | { 218 | "bufferView" : 2, 219 | "componentType" : 5126, 220 | "count" : 70, 221 | "type" : "VEC2" 222 | }, 223 | { 224 | "bufferView" : 3, 225 | "componentType" : 5123, 226 | "count" : 108, 227 | "type" : "SCALAR" 228 | } 229 | ], 230 | "bufferViews" : [ 231 | { 232 | "buffer" : 0, 233 | "byteLength" : 840, 234 | "byteOffset" : 0 235 | }, 236 | { 237 | "buffer" : 0, 238 | "byteLength" : 840, 239 | "byteOffset" : 840 240 | }, 241 | { 242 | "buffer" : 0, 243 | "byteLength" : 560, 244 | "byteOffset" : 1680 245 | }, 246 | { 247 | "buffer" : 0, 248 | "byteLength" : 216, 249 | "byteOffset" : 2240 250 | } 251 | ], 252 | "buffers" : [ 253 | { 254 | "byteLength" : 2456, 255 | "uri" : "data:application/octet-stream;base64,dgJcQOl12kAgICU8dgJcQGN+qkDgHyU87uGfQGN+qkDgHyU87uGfQOl12kAgICU8dgJcQGN+qkDgHyU8dgJcQBRiRkCAHyU87uGfQBRiRkCAHyU87uGfQGN+qkDgHyU8dgJcQBRiRkCAHyU8dgJcQDFOGkBgHyU87uGfQDFOGkBgHyU87uGfQBRiRkCAHyU8dgJcQDFOGkBgHyU8dgJcQCPWmz4AHyU87uGfQCTWmz4AHyU87uGfQDFOGkBgHyU8dgJcQCPWmz4AHyU8dgJcQBmxBr/gHiU87uGfQBixBr/gHiU87uGfQCTWmz4AHyU8dgJcQBmxBr/gHiU8dgJcQDC0OMCAHiU87uGfQDC0OMCAHiU87uGfQBixBr/gHiU8dgJcQDC0OMCAHiU8dgJcQN1rlMAgHiU87uGfQN1rlMAgHiU87uGfQDC0OMCAHiU8hAmlwDC0OMCAHiU8hAmlwN1rlMAgHiU8cSxbwN1rlMAgHiU8cSxbwDC0OMCAHiU8hAmlwByxBr/gHiU8hAmlwDC0OMCAHiU8cSxbwDC0OMCAHiU8cSxbwBuxBr/gHiU8hAmlwCDWmz4AHyU8hAmlwByxBr/gHiU8cSxbwBuxBr/gHiU8cSxbwCHWmz4AHyU8hAmlwDFOGkBgHyU8hAmlwCDWmz4AHyU8cSxbwCHWmz4AHyU8cSxbwDFOGkBgHyU8hAmlwBRiRkCAHyU8hAmlwDFOGkBgHyU8cSxbwDFOGkBgHyU8cSxbwBRiRkCAHyU8hAmlwGN+qkDgHyU8hAmlwBRiRkCAHyU8cSxbwBRiRkCAHyU8cSxbwGN+qkDgHyU8hAmlwOl12kAgICU8hAmlwGN+qkDgHyU8cSxbwGN+qkDgHyU8cSxbwOl12kAgICU8cSxbwDC0OMCAHiU8cSxbwN1rlMAgHiU8dgJcQN1rlMAgHiU8dgJcQDC0OMCAHiU8cSxbwCHWmz4AHyU8cSxbwBuxBr/gHiU8dgJcQBmxBr/gHiU8dgJcQCPWmz4AHyU8cSxbwBRiRkCAHyU8cSxbwDFOGkBgHyU8dgJcQDFOGkBgHyU8dgJcQBRiRkCAHyU8cSxbwOl12kAgICU8cSxbwGN+qkDgHyU8AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/MO2bJ5FHNrP//38/MO2bJ5FHNrP//38/MO2bJ5FHNrP//38/MO2bJ5FHNrP//38/rsJFJmAfGrMAAIA/rsJFJmAfGrMAAIA/rsJFJmAfGrMAAIA/rsJFJmAfGrMAAIA/AAAAAL64IrMAAIA/AAAAAL64IrMAAIA/AAAAAL64IrMAAIA/AAAAAL64IrMAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/AAAAAC0oW7MAAIA/AAAAAC0oW7MAAIA/AAAAAC0oW7MAAIA/AAAAAC0oW7MAAIA/AAAAAL+4IrMAAIA/AAAAAL+4IrMAAIA/AAAAAL+4IrMAAIA/AAAAAL+4IrMAAIA/z+IxJl8fGrP//38/z+IxJl8fGrP//38/z+IxJl8fGrP//38/z+IxJl8fGrP//38/AAAAAJBHNrMAAIA/AAAAAJBHNrMAAIA/AAAAAJBHNrMAAIA/AAAAAJBHNrMAAIA/AAAAAIzaObMAAIA/AAAAAIzaObMAAIA/AAAAAIzaObMAAIA/AAAAAIzaObMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAAElWLLMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/AAAAACwoW7MAAIA/F62zJV8fGrP//38/F62zJV8fGrP//38/F62zJV8fGrP//38/F62zJV8fGrP//38/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/AAAAAIvaObMAAIA/AAAAANTIKrMAAIA/AAAAANTIKrMAAIA/tbNYPwAAAAC2s1g/QOEFPgAAgD9A4QU+AACAPwAAAAC2s1g/QOEFPrazWD/8caY+AACAP/xxpj4AAIA/QOEFPrazWD/8caY+trNYP5AzxT4AAIA/kDPFPgAAgD/8caY+trNYP5AzxT61s1g/u6MRPwAAgD+7oxE/AACAP5AzxT61s1g/u6MRP7WzWD8KLyQ/AACAPwovJD8AAIA/u6MRP7WzWD8KLyQ/trNYP1fgWD8AAIA/V+BYPwAAgD8KLyQ/trNYP1fgWD+1s1g/AACAPwAAgD8AAIA/AACAP1fgWD8AAAAAVuBYPwAAAAAAAIA/JsEuPgAAgD8nwS4+VuBYPwAAAAAJLyQ/AAAAAFbgWD8nwS4+VuBYPyfBLj4JLyQ/AAAAALqjET8AAAAACS8kPyfBLj4JLyQ/J8EuPrqjET8AAAAAjDPFPgAAAAC6oxE/J8EuPrqjET8nwS4+jDPFPgAAAAD4caY+AAAAAIwzxT4nwS4+jDPFPifBLj74caY+AAAAADzhBT4AAAAA+HGmPifBLj74caY+J8EuPkDhBT4AAAAAAAAAAAAAAAA84QU+J8EuPkDhBT4owS4+AAAAACfBLj5W4Fg/JsEuPgAAgD+1s1g/AACAP7azWD9X4Fg/J8EuPrqjET8nwS4+CS8kP7WzWD8KLyQ/tbNYP7ujET8nwS4++HGmPifBLj6MM8U+trNYP5AzxT62s1g//HGmPijBLj4AAAAAJ8EuPkDhBT4AAAEAAgAAAAIAAwAEAAUABgAEAAYABwAIAAkACgAIAAoACwAMAA0ADgAMAA4ADwAQABEAEgAQABIAEwAUABUAFgAUABYAFwAYABkAGgAYABoAGwAcAB0AHgAcAB4AHwAgACEAIgAgACIAIwAkACUAJgAkACYAJwAoACkAKgAoACoAKwAsAC0ALgAsAC4ALwAwADEAMgAwADIAMwA0ADUANgA0ADYANwA4ADkAOgA4ADoAOwA8AD0APgA8AD4APwBAAEEAQgBAAEIAQwBEAEUAAQBEAAEAAAA=" 256 | } 257 | ] 258 | } 259 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moz_text_test", 3 | "version": "1.0.0", 4 | "description": "test for MOZ_text glTF extension", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "webpack-dev-server -d --host 0.0.0.0", 11 | "build": "webpack -p", 12 | "watch": "webpack --watch", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": {}, 16 | "keywords": [ 17 | "gltf", 18 | "text", 19 | "extension", 20 | "threejs" 21 | ], 22 | "author": "Diego F. Goberna", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@babel/core": "^7.9.0", 26 | "babel-loader": "^8.1.0", 27 | "dat.gui": "^0.7.7", 28 | "three": "^0.115.0", 29 | "troika-3d-text": "^0.22.0", 30 | "webpack": "^4.42.1", 31 | "webpack-dev-server": "^3.10.1" 32 | }, 33 | "devDependencies": { 34 | "webpack-cli": "^3.3.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: './index.js', 4 | output: { 5 | filename: 'bundle.js', 6 | path: __dirname 7 | }, 8 | devtool: 'source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(js|mjs)$/, 13 | exclude: /(node_modules)/, 14 | use: { loader: 'babel-loader' } 15 | } 16 | ] 17 | }, 18 | watchOptions: { 19 | ignored: [/node_modules/], 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /threejs/README.md: -------------------------------------------------------------------------------- 1 | 2 | This is a modification to the GLTFLoader.js to support the MOZ_text 3 | --------------------------------------------------------------------------------