├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── codeql.yml │ └── node.js.yml ├── .gitignore ├── LICENSE ├── NOTES.md ├── README.md ├── dist ├── example.e31bb0bc.js ├── example.e31bb0bc.js.map ├── index.a869dc96.js ├── index.a869dc96.js.map └── index.html ├── example ├── SkinWeightsShaderMixin.js ├── index.html ├── index.js └── package.json ├── images └── banner.png ├── package-lock.json ├── package.json ├── scripts ├── generate-header-read.js └── header.txt └── src ├── MDLLoader.js ├── SourceModelLoader.js ├── VMTLoader.js ├── VTFLoader.js ├── VTXLoader.js └── VVDLoader.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = tab 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "./node_modules/eslint-config-mdcs/index.js", 4 | "rules": { 5 | "no-throw-literal": [ 6 | "error" 7 | ], 8 | "prefer-const": [ 9 | "error", 10 | { 11 | "destructuring": "any", 12 | "ignoreReadBeforeAssign": false 13 | } 14 | ], 15 | "quotes": [ "error", "single" ], 16 | "indent": [ "error", "tab" ], 17 | "no-var": [ "error" ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "57 15 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run lint 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | local-models/* 64 | source-engine-model-loader-models 65 | .cache 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Garrett Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Valve File Loaders 2 | 3 | A small effort to try to decode and load valve 3d file formats. 4 | 5 | Get models from [SFMLab](https://SFMLab.com). 6 | 7 | ## Formats 8 | 9 | - **VTF** https://developer.valvesoftware.com/wiki/Valve_Texture_Format 10 | - Texture Format 11 | - **VMT** https://developer.valvesoftware.com/wiki/Material 12 | - Material Format 13 | - **VTX** https://developer.valvesoftware.com/wiki/VTX 14 | - Per-target optimized representations of vertex indices 15 | - **VVD** https://developer.valvesoftware.com/wiki/VVD 16 | - Bone, normal, position, uv, attribute information 17 | - **PHY** https://developer.valvesoftware.com/wiki/PHY 18 | - Physics and ragdoll information 19 | - **MDL** https://developer.valvesoftware.com/wiki/MDL 20 | - The high level information for defining the structure of a model 21 | 22 | ## Open Questions 23 | - ~How do you use the `vertOffset` field in the `strips` objects?~ 24 | - Seem to not be used. 25 | - Mark the beginning and ends of structs in memory to make sure everything lines up as expected. 26 | - How large are the strides for the structs? Why do some seem to be longer than others? 27 | - The size of in the structs seems to be what was expected but it doesn't explain the apparent need for padding 28 | - Read in a file and use the structs to read the data and make sure it makes sense and lines up 29 | - With multiple triangle strips being used it might be best to use the primitive restart approach: https://stackoverflow.com/questions/4386861/opengl-jogl-multiple-triangle-fans-in-a-vertex-array 30 | - When animating bones: https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/sp/src/public/bone_setup.cpp 31 | 32 | ## TODO 33 | - Parse Bone Structure 34 | 35 | ## Other References 36 | 37 | ### [Studio.h](https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/public/studio.h) 38 | 39 | Source with struct definitions used in the `mdl` and other files. 40 | 41 | ### [Optimize.h](https://github.com/ValveSoftware/source-sdk-2013/blob/master/mp/src/public/optimize.h) 42 | 43 | Source with struct definitions used in the `vtx` files. 44 | 45 | ### [Vradstaticprops.cpp](https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/utils/vrad/vradstaticprops.cpp) 46 | 47 | Source with some file loading vertex reading logic. 48 | 49 | Especially the loop defined at [lines 1504-1688](https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/utils/vrad/vradstaticprops.cpp#L1504-L1688), which shows the relationship between the MDL and VTX file data. 50 | 51 | ## Things To Note 52 | - THREE assumes counter clockwise triangle winding order while DirectX (and probably the vtx files) [assume clockwise order](https://stackoverflow.com/questions/23790272/vertex-winding-order-in-dx11). 53 | - There's a lot of indirection in the way vertex data is defined. Look in the loop and at the `mstudio_meshvertexdata_t` struct in `studio.h` to unpack it. 54 | 55 | ## Understanding Vertices 56 | ### Key Structs and Functions 57 | #### .VVD 58 | Defined in `studio.h`. 59 | 60 | ##### struct vertexFileHeader_t 61 | The header provides a pointer to a buffer of interlaced bone weight, position, normal, and uv data. 62 | 63 | ##### struct mstudiovertex_t 64 | Defines the data layout for the vertex data buffer. 65 | 66 | ##### struct mstudio_modelvertexdata_t 67 | The struct to access all the vertex data in the given buffer via functions like `Position(i)`, `Normal(i)`, `Vertex(i)`, etc. 68 | 69 | TODO: To get the vertex index the function `GetGlobalVertexIndex` is used. 70 | 71 | #### .MDL 72 | Defined in `studio.h`. 73 | 74 | ##### struct studiohdr_t 75 | Provides pointers to various model data including texture data and "body parts". 76 | 77 | ##### struct mstudiobodyparts_t 78 | Defines a groups of models. 79 | 80 | ##### struct mstudiomodel_t 81 | Defines a group of meshes and contains a handle to the `mstudio_modelvertexdata_t` from the VVD class (right??). 82 | 83 | ##### struct mstudiomesh_t 84 | Defines a mesh to be rendered and contains a handle to `mstudio_meshvertexdata_t` (_NOT_ model), which indirectly accesses data in the `mstudio_modelvertexdata_t` struct of the meshes parent model. 85 | 86 | This struct defines `vertoffset` and `vertindex` to index into the model data. Possibly this is cached or duplicated data from the VTX strip data? 87 | 88 | ##### struct mstudio_meshvertexdata_t 89 | Defines accessors into the model vertex data for vertices using the `vertOffset` field (see `getModelVertexIndex` function). 90 | 91 | #### .VTX 92 | Defined in `optimize.h` 93 | 94 | Defines indices and render approaches for the model in a set of bodyparts, meshes, strips, etc that mirrors the structure of the data in the MDL file. 95 | 96 | ### Putting it all together 97 | 98 | From the loop linked to above the order to go from strip to final vertex is as follows: 99 | 100 | ```c 101 | // From Strip 102 | // iterate over every index in the strip 103 | index = pStrip -> indexOffset + i; 104 | 105 | // From StripGroup 106 | // index into the strip group's index buffer (defined as `(byte*)this + this.indexoffset)`) 107 | // see stripGroup -> pIndex 108 | index2 = pStripGroup -> indexBuffer[ index ]; 109 | 110 | // index into the group's vertex buffer (defined as `(byte*)this + this.vertoffset)`) 111 | // see stripGroup -> pVertex 112 | index3 = pStripGroup -> vertexBuffer[ index2 ] -> origMeshVertID; 113 | 114 | // From mstudiomesh_t 115 | // index into the meshes indices 116 | // see mstudio_meshvertexdata_t::Position and mstudio_meshvertexdata_t::GetModelVertexIndex 117 | index4 = pMesh -> vertexoffset + index3; 118 | 119 | // From mstudiomodel_t 120 | // see mstudio_modelvertexdata_t::Position and mstudio_modelvertexdata_t::GetGlobalVertexIndex 121 | index5 = index4 + pModel -> vertexindex / sizeof( vertex ); 122 | ``` 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # source-engine-model-loader 2 | 3 | [![build](https://img.shields.io/github/actions/workflow/status/gkjohnson/source-engine-model-loader/node.js.yml?style=flat-square&label=build&branch=master)](https://github.com/gkjohnson/source-engine-model-loader/actions) 4 | [![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/gkjohnson/source-engine-model-loader/) 5 | [![twitter](https://flat.badgen.net/badge/twitter/@garrettkjohnson/?icon&label)](https://twitter.com/garrettkjohnson) 6 | 7 | Unofficial Three.js loader for parsing Valve's Source Engine models built by referencing the data structures in the [ValveSoftware/source-sdk-2013](https://github.com/ValveSoftware/source-sdk-2013) project and the source engine [wiki](https://developer.valvesoftware.com/wiki/MDL). Demo models from the [Source Filmmaker](https://store.steampowered.com/app/1840/Source_Filmmaker/) installation. 8 | 9 | ![](./images/banner.png) 10 | 11 | Get models from [SFMLab](https://SFMLab.com) or [Source Filmmaker](https://store.steampowered.com/app/1840/Source_Filmmaker/). 12 | 13 | Demo [here!](https://gkjohnson.github.io/source-engine-model-loader/dist/index.html) 14 | 15 | # License Information 16 | 17 | Models shown in this repo are not covered by the code license, copyright their respective owners, and are for demo purposes only. 18 | 19 | # Use 20 | 21 | ```js 22 | import { SourceModelLoader } from 'source-engine-model-loader'; 23 | 24 | new SourceModelLoader() 25 | .load( './folder/model', ( { group } ) => { 26 | 27 | scene.add( group ); 28 | 29 | } ); 30 | ``` 31 | 32 | # API 33 | 34 | ## SourceModelLoader 35 | 36 | ### constructor 37 | 38 | ```js 39 | constructor( manager : LoadingManager ) 40 | ``` 41 | 42 | ### load 43 | 44 | ```js 45 | load( 46 | url : string, 47 | onComplete : ( { group : Group } ) => void 48 | ) : void 49 | ``` 50 | 51 | Loads the set of Source Engine model files at the given path. It is expected that a model with the extensions `.mdl`, `.vvd`, and `.vtx` exist. 52 | 53 | # Unimplemented Features 54 | 55 | See [issue #4](https://github.com/gkjohnson/source-engine-model-loader/issues/4) for full list of unimplemented features. 56 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | threejs webgl - Source Film Maker Loader
Valve Source Engine Model Loader for three.js by Garrett Johnson
model from Valve's Source Filmmaker copyright the respective owner.

Use "W", "E", and "R" to change transform controls mode.
-------------------------------------------------------------------------------- /example/SkinWeightsShaderMixin.js: -------------------------------------------------------------------------------- 1 | import { Color, UniformsUtils } from 'three'; 2 | 3 | function cloneShader( shader, uniforms, defines ) { 4 | 5 | const newShader = Object.assign( {}, shader ); 6 | newShader.uniforms = UniformsUtils.merge( [ 7 | newShader.uniforms, 8 | uniforms 9 | ] ); 10 | newShader.defines = Object.assign( {}, defines ); 11 | 12 | return newShader; 13 | 14 | } 15 | 16 | export function SkinWeightMixin( shader ) { 17 | 18 | const defineKeyword = 'ENABLE_SKIN_WEIGHTS'; 19 | const newShader = cloneShader( 20 | shader, 21 | { 22 | skinWeightColor: { value: new Color() }, 23 | skinWeightOpacity: { value: 1.0 }, 24 | skinWeightIndex: { value: - 1 } 25 | }, 26 | { 27 | [ defineKeyword ]: 1, 28 | }, 29 | ); 30 | 31 | newShader.vertexShader = ` 32 | uniform float skinWeightIndex; 33 | varying float skinWeightColorRatio; 34 | ${newShader.vertexShader} 35 | `.replace( 36 | /#include /, 37 | v => `${v} 38 | { 39 | #ifdef ENABLE_SKIN_WEIGHTS 40 | skinWeightColorRatio += skinWeight.x * float( skinIndex.x == skinWeightIndex ); 41 | skinWeightColorRatio += skinWeight.y * float( skinIndex.y == skinWeightIndex ); 42 | skinWeightColorRatio += skinWeight.z * float( skinIndex.z == skinWeightIndex ); 43 | skinWeightColorRatio += skinWeight.w * float( skinIndex.w == skinWeightIndex ); 44 | #endif 45 | } 46 | ` 47 | ); 48 | 49 | newShader.fragmentShader = ` 50 | uniform vec3 skinWeightColor; 51 | varying float skinWeightColorRatio; 52 | ${newShader.fragmentShader} 53 | `.replace( 54 | /vec4 diffuseColor = vec4\( diffuse, opacity \);/, 55 | v => `${v} 56 | #ifdef ENABLE_SKIN_WEIGHTS 57 | diffuseColor = vec4( skinWeightColor, smoothstep( 0.1, 0.3, skinWeightColorRatio ) * opacity ); 58 | #endif 59 | `, 60 | ); 61 | 62 | return newShader; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | threejs webgl - Source Film Maker Loader 5 | 6 | 7 | 31 | 32 | 33 | 34 |
35 | Valve Source Engine Model Loader for three.js by Garrett Johnson 36 |
37 | model from Valve's Source Filmmaker copyright the respective owner. 38 |
39 |
40 |
Use "W", "E", and "R" to change transform controls mode.
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | SkeletonHelper, 3 | WebGLRenderer, 4 | PerspectiveCamera, 5 | Scene, 6 | DirectionalLight, 7 | HemisphereLight, 8 | Mesh, 9 | DoubleSide, 10 | Box3, 11 | PlaneBufferGeometry, 12 | ShadowMaterial, 13 | ShaderLib, 14 | LinearEncoding, 15 | Vector3, 16 | Raycaster, 17 | Vector2, 18 | ShaderMaterial, 19 | Triangle, 20 | PCFSoftShadowMap, 21 | Sphere, 22 | } from 'three'; 23 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 24 | import { SourceModelLoader } from '../src/SourceModelLoader.js'; 25 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; 26 | import { SkinWeightMixin } from './SkinWeightsShaderMixin.js'; 27 | import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; 28 | 29 | // globals 30 | const params = { 31 | 32 | showSkeleton: false, 33 | skin: 0, 34 | selectParentBoneWithChildren: true, 35 | 36 | }; 37 | const MODELS = { 38 | 39 | Atlas: './models/portal2/models/player/ballbot/ballbot', 40 | PBody: './models/portal2/models/player/eggbot/eggbot', 41 | Turret: './models/portal2/models/npcs/turret/turret', 42 | 43 | Engineer: './models/tf/models/player/engineer', 44 | Pyro: './models/tf/models/player/pyro', 45 | Spy: './models/tf/models/player/spy', 46 | Demoman: './models/tf/models/player/demo', 47 | Heavy: './models/tf/models/player/heavy', 48 | Medic: './models/tf/models/player/medic', 49 | Scout: './models/tf/models/player/scout', 50 | Sniper: './models/tf/models/player/sniper', 51 | Soldier: './models/tf/models/player/soldier', 52 | 53 | }; 54 | 55 | let camera, scene, renderer, controls; 56 | let directionalLight, ambientLight, ground; 57 | let skeletonHelper, model, skeleton, gui; 58 | let transformControls; 59 | const mouse = new Vector2(); 60 | const mouseDown = new Vector2(); 61 | const unselectableBones = []; 62 | let movingControls = false; 63 | 64 | const SkinWeightShader = SkinWeightMixin( ShaderLib.phong ); 65 | const skinWeightsMaterial = new ShaderMaterial( SkinWeightShader ); 66 | skinWeightsMaterial.polygonOffset = true; 67 | skinWeightsMaterial.polygonOffsetFactor = - 1; 68 | skinWeightsMaterial.polygonOffsetUnits = - 1; 69 | skinWeightsMaterial.lights = true; 70 | skinWeightsMaterial.skinning = true; 71 | skinWeightsMaterial.transparent = true; 72 | skinWeightsMaterial.depthWrite = false; 73 | skinWeightsMaterial.uniforms.skinWeightColor.value.set( 0xe91e63 ); 74 | skinWeightsMaterial.uniforms.emissive.value.set( 0xe91e63 ).multiplyScalar( 0.5 ); 75 | skinWeightsMaterial.uniforms.opacity.value = 0.75; 76 | skinWeightsMaterial.uniforms.shininess.value = 0.01; 77 | 78 | let loadingId = 0; 79 | 80 | 81 | const raycastBones = ( function () { 82 | 83 | const raycaster = new Raycaster(); 84 | const triangle = new Triangle(); 85 | const baryCoord = new Vector3(); 86 | const getFunctions = [ 'getX', 'getY', 'getZ', 'getW' ]; 87 | 88 | return function ( mousePos, giveDirectBone = false ) { 89 | 90 | if ( model && ! transformControls.object ) { 91 | 92 | raycaster.setFromCamera( mousePos, camera ); 93 | 94 | const res = raycaster.intersectObject( model, true ); 95 | if ( res.length ) { 96 | 97 | const hit = res[ 0 ]; 98 | const object = hit.object; 99 | const geometry = object.geometry; 100 | const face = hit.face; 101 | 102 | const skinWeightAttr = geometry.getAttribute( 'skinWeight' ); 103 | const skinIndexAttr = geometry.getAttribute( 'skinIndex' ); 104 | const positionAttr = geometry.getAttribute( 'position' ); 105 | const weightTotals = {}; 106 | 107 | const aIndex = face.a; 108 | const bIndex = face.b; 109 | const cIndex = face.c; 110 | 111 | triangle.a.fromBufferAttribute( positionAttr, aIndex ); 112 | triangle.b.fromBufferAttribute( positionAttr, bIndex ); 113 | triangle.c.fromBufferAttribute( positionAttr, cIndex ); 114 | 115 | object.boneTransform( aIndex, triangle.a ); 116 | object.boneTransform( bIndex, triangle.b ); 117 | object.boneTransform( cIndex, triangle.c ); 118 | 119 | triangle.a.applyMatrix4( object.matrixWorld ); 120 | triangle.b.applyMatrix4( object.matrixWorld ); 121 | triangle.c.applyMatrix4( object.matrixWorld ); 122 | 123 | triangle.getBarycoord( hit.point, baryCoord ); 124 | for ( let i = 0; i < skinIndexAttr.itemSize; i ++ ) { 125 | 126 | const func = getFunctions[ i ]; 127 | const aWeightIndex = skinIndexAttr[ func ]( aIndex ); 128 | const bWeightIndex = skinIndexAttr[ func ]( bIndex ); 129 | const cWeightIndex = skinIndexAttr[ func ]( cIndex ); 130 | const aWeight = skinWeightAttr[ func ]( aIndex ); 131 | const bWeight = skinWeightAttr[ func ]( bIndex ); 132 | const cWeight = skinWeightAttr[ func ]( cIndex ); 133 | 134 | weightTotals[ aWeightIndex ] = weightTotals[ aWeightIndex ] || 0; 135 | weightTotals[ bWeightIndex ] = weightTotals[ bWeightIndex ] || 0; 136 | weightTotals[ cWeightIndex ] = weightTotals[ cWeightIndex ] || 0; 137 | 138 | weightTotals[ aWeightIndex ] += aWeight * baryCoord.x; 139 | weightTotals[ bWeightIndex ] += bWeight * baryCoord.y; 140 | weightTotals[ cWeightIndex ] += cWeight * baryCoord.z; 141 | 142 | } 143 | 144 | const sorted = 145 | Object 146 | .entries( weightTotals ) 147 | .map( ( [ key, value ] ) => ( { weight: value, index: key } ) ) 148 | .sort( ( a, b ) => b.weight - a.weight ); 149 | 150 | const boneIndex = sorted[ 0 ].index; 151 | let bone = skeleton.bones[ boneIndex ]; 152 | const parentIndex = skeleton.bones.findIndex( b => b === bone.parent ); 153 | 154 | // TODO: this should check if the parent bone isn't clickable through any other means 155 | // then we should bump to the parent. 156 | if ( 157 | params.selectParentBoneWithChildren && 158 | unselectableBones.includes( parentIndex ) && 159 | bone.children.length === 0 && 160 | bone.parent.children.length > 1 && 161 | ! giveDirectBone 162 | ) { 163 | 164 | bone = bone.parent; 165 | 166 | } 167 | 168 | return bone; 169 | 170 | } else { 171 | 172 | return null; 173 | 174 | } 175 | 176 | } 177 | 178 | return null; 179 | 180 | }; 181 | 182 | } )(); 183 | 184 | init(); 185 | rebuildGui(); 186 | animate(); 187 | 188 | function loadModel( path ) { 189 | 190 | if ( model ) { 191 | 192 | model.traverse( c => { 193 | 194 | if ( c.material ) { 195 | 196 | c.material.dispose(); 197 | for ( const key in c.material ) { 198 | 199 | if ( c.material[ key ] && c.material[ key ].isTexture ) { 200 | 201 | c.material[ key ].dispose(); 202 | 203 | } 204 | 205 | } 206 | 207 | } 208 | 209 | if ( c.geometry ) { 210 | 211 | c.geometry.dispose(); 212 | 213 | } 214 | 215 | if ( c.skeleton ) { 216 | 217 | c.skeleton.dispose(); 218 | 219 | } 220 | 221 | } ); 222 | model.parent.remove( model ); 223 | skeletonHelper.parent.remove( skeletonHelper ); 224 | 225 | model = null; 226 | skeletonHelper = null; 227 | skeleton = null; 228 | 229 | } 230 | 231 | params.model = path; 232 | 233 | loadingId ++; 234 | const myLoadingId = loadingId; 235 | 236 | new SourceModelLoader() 237 | .load( 238 | new URL( path, 'https://raw.githubusercontent.com/gkjohnson/source-engine-model-loader-models/master/' ).toString(), 239 | ( { group, vvd, vtx, mdl } ) => { 240 | 241 | if ( loadingId !== myLoadingId ) return; 242 | 243 | window.vvd = vvd; 244 | window.vtx = vtx; 245 | window.mdl = mdl; 246 | window.group = group; 247 | 248 | group.traverse( c => { 249 | 250 | if ( c.isSkinnedMesh ) { 251 | 252 | // Find the bone indices that are unreferenced in the model 253 | const getFunctions = [ 'getX', 'getY', 'getZ', 'getW' ]; 254 | const geometry = c.geometry; 255 | const skinWeightAttr = geometry.getAttribute( 'skinWeight' ); 256 | const skinIndexAttr = geometry.getAttribute( 'skinIndex' ); 257 | const weightMap = []; 258 | let overallTotalWeight = 0; 259 | 260 | for ( let i = 0, l = skinWeightAttr.count; i < l; i ++ ) { 261 | 262 | let maxWeight = 0; 263 | let maxIndex = - 1; 264 | for ( let j = 0, jl = skinIndexAttr.itemSize; j < jl; j ++ ) { 265 | 266 | const func = getFunctions[ j ]; 267 | const weightIndex = skinIndexAttr[ func ]( i ); 268 | const weight = skinWeightAttr[ func ]( i ); 269 | if ( weight > maxWeight ) { 270 | 271 | maxWeight = weight; 272 | maxIndex = weightIndex; 273 | 274 | } 275 | 276 | } 277 | 278 | let weightInfo = weightMap[ maxIndex ]; 279 | if ( ! weightInfo ) { 280 | 281 | weightInfo = { totalCount: 0, totalWeight: 0 }; 282 | weightMap[ maxIndex ] = weightInfo; 283 | 284 | } 285 | 286 | weightInfo.totalCount ++; 287 | weightInfo.totalWeight += maxWeight; 288 | overallTotalWeight += maxWeight; 289 | 290 | } 291 | 292 | const mappedWeights = weightMap.map( info => info ? info.totalWeight / overallTotalWeight : 0 ); 293 | for ( let i = 0; i < mappedWeights.length; i ++ ) { 294 | 295 | if ( ! mappedWeights[ i ] ) { 296 | 297 | unselectableBones.push( i ); 298 | 299 | } 300 | 301 | } 302 | 303 | } 304 | 305 | } ); 306 | 307 | skeletonHelper = new SkeletonHelper( group ); 308 | scene.add( skeletonHelper ); 309 | scene.add( group ); 310 | group.traverse( c => { 311 | 312 | if ( c.isMesh ) { 313 | 314 | c.castShadow = true; 315 | c.receiveShadow = true; 316 | 317 | } 318 | 319 | if ( c.isSkinnedMesh ) { 320 | 321 | skeleton = c.skeleton; 322 | 323 | } 324 | 325 | } ); 326 | 327 | const bb = new Box3(); 328 | bb.setFromObject( group ); 329 | 330 | const sphere = new Sphere(); 331 | bb.getBoundingSphere( sphere ); 332 | 333 | group.scale.multiplyScalar( 20 / sphere.radius ); 334 | const dim = new Vector3().subVectors( bb.max, bb.min ); 335 | if ( dim.z > dim.y ) { 336 | 337 | group.rotation.x = - Math.PI / 2; 338 | 339 | } 340 | 341 | bb.setFromObject( group ).getCenter( group.position ).multiplyScalar( - 1 ); 342 | bb.setFromObject( group ); 343 | 344 | bb.getCenter( directionalLight.position ); 345 | directionalLight.position.x += 20; 346 | directionalLight.position.y += 30; 347 | directionalLight.position.z += 20; 348 | 349 | ground.position.y = bb.min.y; 350 | 351 | const cam = directionalLight.shadow.camera; 352 | cam.left = cam.bottom = - 20; 353 | cam.right = cam.top = 20; 354 | cam.updateProjectionMatrix(); 355 | 356 | // Expand the bounding volumes by a ton so that parts can't be dragged outside the 357 | // raycast volume. 358 | group.traverse( c => { 359 | 360 | if ( c.isSkinnedMesh ) { 361 | 362 | if ( ! c.geometry.boundingBox ) c.geometry.computeBoundingBox(); 363 | c.geometry.boundingBox.min.multiplyScalar( 1000 ); 364 | c.geometry.boundingBox.max.multiplyScalar( 1000 ); 365 | 366 | if ( ! c.geometry.boundingSphere ) c.geometry.computeBoundingSphere(); 367 | c.geometry.boundingSphere.radius *= 1000; 368 | 369 | } 370 | 371 | } ); 372 | 373 | model = group; 374 | rebuildGui(); 375 | 376 | } ); 377 | 378 | } 379 | 380 | function init() { 381 | 382 | renderer = new WebGLRenderer( { antialias: true } ); 383 | renderer.setPixelRatio( window.devicePixelRatio ); 384 | renderer.setSize( window.innerWidth, window.innerHeight ); 385 | renderer.setClearColor( 0x0d1113 ); 386 | renderer.outputEncoding = LinearEncoding; 387 | renderer.shadowMap.enabled = true; 388 | renderer.shadowMap.type = PCFSoftShadowMap; 389 | document.body.appendChild( renderer.domElement ); 390 | 391 | // initialize renderer, scene, camera 392 | camera = new PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 6000 ); 393 | camera.position.set( 20, 20, 60 ); 394 | 395 | scene = new Scene(); 396 | 397 | directionalLight = new DirectionalLight( 0xFFF8E1, 1.0 ); 398 | directionalLight.position.set( 1, 3, - 2 ).multiplyScalar( 100 ); 399 | directionalLight.castShadow = true; 400 | directionalLight.shadow.mapSize.setScalar( 1024 ); 401 | 402 | const dlShadowCam = directionalLight.shadow.camera; 403 | dlShadowCam.left = dlShadowCam.bottom = - 100; 404 | dlShadowCam.top = dlShadowCam.right = 100; 405 | scene.add( directionalLight ); 406 | 407 | ambientLight = new HemisphereLight( 0xE0F7FA, 0x8D6E63, 0.45 ); 408 | scene.add( ambientLight ); 409 | 410 | ground = new Mesh( new PlaneBufferGeometry() ); 411 | ground.material = new ShadowMaterial( { side: DoubleSide, opacity: 0.5, transparent: true, depthWrite: false } ); 412 | ground.receiveShadow = true; 413 | ground.scale.multiplyScalar( 1000 ); 414 | ground.rotation.x = - Math.PI / 2; 415 | scene.add( ground ); 416 | 417 | loadModel( MODELS[ 'Pyro' ] ); 418 | 419 | // camera controls 420 | 421 | renderer.domElement.addEventListener( 'pointermove', onMouseMove, false ); 422 | renderer.domElement.addEventListener( 'pointerdown', onMouseDown, false ); 423 | renderer.domElement.addEventListener( 'pointerup', onMouseUp, false ); 424 | 425 | controls = new OrbitControls( camera, renderer.domElement ); 426 | controls.addEventListener( 'start', () => movingControls = true ); 427 | controls.addEventListener( 'end', () => movingControls = false ); 428 | controls.minDistance = 5; 429 | controls.maxDistance = 3000; 430 | 431 | transformControls = new TransformControls( camera, renderer.domElement ); 432 | transformControls.mode = 'rotate'; 433 | transformControls.space = 'local'; 434 | transformControls.size = 0.75; 435 | transformControls.addEventListener( 'dragging-changed', e => { 436 | 437 | controls.enabled = ! e.value; 438 | movingControls = false; 439 | 440 | } ); 441 | scene.add( transformControls ); 442 | 443 | window.addEventListener( 'resize', onWindowResize, false ); 444 | window.addEventListener( 'keydown', e => { 445 | 446 | switch ( e.key ) { 447 | 448 | case 'w': 449 | transformControls.mode = 'translate'; 450 | break; 451 | case 'e': 452 | transformControls.mode = 'rotate'; 453 | break; 454 | case 'r': 455 | transformControls.mode = 'scale'; 456 | break; 457 | 458 | } 459 | 460 | } ); 461 | 462 | } 463 | 464 | function rebuildGui() { 465 | 466 | if ( gui ) { 467 | 468 | gui.destroy(); 469 | 470 | } 471 | 472 | params.skin = 0; 473 | 474 | // dat gui 475 | gui = new GUI(); 476 | gui.width = 400; 477 | 478 | gui.add( params, 'model', MODELS ).onChange( loadModel ); 479 | 480 | gui.add( params, 'showSkeleton' ); 481 | gui.add( params, 'selectParentBoneWithChildren' ); 482 | 483 | if ( model ) { 484 | 485 | const options = {}; 486 | model.userData.skinsTable.forEach( ( arr, i ) => { 487 | 488 | options[ `skin ${ i }` ] = i; 489 | 490 | } ); 491 | 492 | gui.add( params, 'skin' ).options( options ); 493 | 494 | } 495 | 496 | gui.open(); 497 | 498 | } 499 | 500 | function onWindowResize() { 501 | 502 | const width = window.innerWidth; 503 | const height = window.innerHeight; 504 | 505 | camera.aspect = width / height; 506 | camera.updateProjectionMatrix(); 507 | 508 | renderer.setSize( width, height ); 509 | 510 | } 511 | 512 | function onMouseMove( e ) { 513 | 514 | mouse.x = ( e.clientX / renderer.domElement.clientWidth ) * 2 - 1; 515 | mouse.y = - ( e.clientY / renderer.domElement.clientHeight ) * 2 + 1; 516 | 517 | } 518 | 519 | function onMouseDown( e ) { 520 | 521 | onMouseMove( e ); 522 | mouseDown.copy( mouse ); 523 | 524 | } 525 | 526 | function onMouseUp( e ) { 527 | 528 | onMouseMove( e ); 529 | if ( mouseDown.distanceTo( mouse ) < 0.001 ) { 530 | 531 | const hitBone = raycastBones( mouse, e.which !== 1 ); 532 | if ( hitBone ) { 533 | 534 | // use right click to select tip bone 535 | transformControls.attach( hitBone ); 536 | 537 | } else { 538 | 539 | transformControls.detach(); 540 | raycastBones( mouse ); 541 | 542 | } 543 | 544 | } 545 | 546 | } 547 | 548 | function animate() { 549 | 550 | requestAnimationFrame( animate ); 551 | render(); 552 | 553 | } 554 | 555 | function render() { 556 | 557 | if ( skeletonHelper ) { 558 | 559 | skeletonHelper.visible = params.showSkeleton; 560 | 561 | } 562 | 563 | if ( model ) { 564 | 565 | const skinsTable = model.userData.skinsTable; 566 | const materials = model.userData.materials; 567 | model.traverse( c => { 568 | 569 | if ( c.isMesh ) { 570 | 571 | c.material = materials[ skinsTable[ params.skin ][ c.userData.materialIndex ] ]; 572 | 573 | } 574 | 575 | } ); 576 | 577 | if ( transformControls.object ) { 578 | 579 | skinWeightsMaterial.uniforms.skinWeightIndex.value = skeleton.bones.indexOf( transformControls.object ); 580 | 581 | } else { 582 | 583 | skinWeightsMaterial.uniforms.skinWeightIndex.value = - 1; 584 | 585 | } 586 | 587 | 588 | } 589 | 590 | controls.update(); 591 | renderer.render( scene, camera ); 592 | 593 | if ( ! movingControls ) { 594 | 595 | const bone = raycastBones( mouse ); 596 | if ( bone ) { 597 | 598 | skinWeightsMaterial.uniforms.skinWeightIndex.value = skeleton.bones.indexOf( bone ); 599 | 600 | } 601 | 602 | } 603 | 604 | if ( model ) { 605 | 606 | model.traverse( c => { 607 | 608 | if ( c.isMesh ) { 609 | 610 | c.material = skinWeightsMaterial; 611 | 612 | } 613 | 614 | } ); 615 | 616 | renderer.autoClear = false; 617 | renderer.render( scene, camera ); 618 | renderer.autoClear = true; 619 | 620 | } 621 | 622 | } 623 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/source-engine-model-loader/f32a6d2c01c0712e863aba27f1f5316c0177722d/images/banner.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-engine-model-loader", 3 | "version": "1.0.0", 4 | "description": "three.js loader for parsing source engine models", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cd example && parcel serve ./*.html --dist-dir ../dist/ --no-cache --no-hmr", 8 | "build": "cd example && parcel build ./*.html --dist-dir ../dist/ --no-cache --no-content-hash --public-url ./", 9 | "lint": "eslint ./{src,example}/**.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/gkjohnson/source-engine-model-loader.git" 14 | }, 15 | "author": "Garrett Johnson ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/gkjohnson/source-engine-model-loader/issues" 19 | }, 20 | "homepage": "https://github.com/gkjohnson/source-engine-model-loader#readme", 21 | "peerDependencies": { 22 | "three": "^0.119.0" 23 | }, 24 | "devDependencies": { 25 | "concurrently": "^5.2.0", 26 | "eslint": "^7.32.0", 27 | "eslint-config-mdcs": "^5.0.0", 28 | "parcel": "^2.0.0", 29 | "static-server": "^2.2.1", 30 | "three": "^0.148.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/generate-header-read.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ); 2 | const fields = []; 3 | const lines = 4 | fs.readFileSync( './header.txt', { encoding: 'utf8' } ) 5 | .replace( /\/\*[\s\S]*?\*\//g, '' ) 6 | .replace( /\/\/.*?[\n\r]/g, '\n' ) 7 | .split( /\n/g ) 8 | .map( l => l.trim() ) 9 | .filter( l => ! ! l ) 10 | .map( l => { 11 | 12 | const tokens = /(mutable)?\s*(unsigned)?\s*(\S+)\s+(\*)?(\w+)(\[)?/.exec( l ); 13 | const unsigned = tokens[ 2 ] || null; 14 | const type = tokens[ 3 ]; 15 | const isPointer = ! ! tokens[ 4 ]; 16 | const name = tokens[ 5 ].replace( /_(\w)/g, ( match, c ) => c.toUpperCase() ); 17 | const isArray = ! ! tokens[ 6 ]; 18 | 19 | const result = [ `// ${ l.replace( /\s+/g, ' ' ) }` ]; 20 | 21 | if ( isPointer ) { 22 | 23 | result.push( `i += 4;` ); 24 | 25 | } else if ( isArray ) { 26 | 27 | result.push( `var ${ name } = UNHANDLED_ARRAY;` ); 28 | fields.push( name ); 29 | 30 | } else { 31 | 32 | let func; 33 | switch ( type ) { 34 | 35 | case 'int': 36 | func = unsigned ? 'getUint32' : 'getInt32'; 37 | result.push( `var ${ name } = dataView.${ func }( i, true );` ); 38 | result.push( `i += 4;` ); 39 | break; 40 | 41 | case 'char': 42 | case 'byte': 43 | func = ( unsigned || type === 'byte' ) ? 'getUint8' : 'getInt8'; 44 | result.push( `var ${ name } = dataView.${ func }( i, true );` ); 45 | result.push( `i += 1;` ); 46 | break; 47 | 48 | case 'float': 49 | result.push( `var ${ name } = dataView.getFloat32( i, true );` ); 50 | result.push( `i += 4;` ); 51 | break; 52 | 53 | case 'short': 54 | func = unsigned ? 'getUint16' : 'getInt16'; 55 | result.push( `var ${ name } = dataView.${ func }( i, true );` ); 56 | result.push( `i += 2;` ); 57 | break; 58 | 59 | case 'long': 60 | result.push( `var ${ name } = dataView.getBigInt64( i, true );` ); 61 | result.push( `i += 8;` ); 62 | break; 63 | 64 | case 'Vector': 65 | result.push( `var ${ name } = new THREE.Vector3();` ); 66 | result.push( `${ name }.x = dataView.getFloat32( i + 0, true );` ); 67 | result.push( `${ name }.y = dataView.getFloat32( i + 4, true );` ); 68 | result.push( `${ name }.z = dataView.getFloat32( i + 8, true );` ); 69 | result.push( `i += 12;` ); 70 | break; 71 | 72 | default: 73 | result.push( `var ${ name } = UNHANDLED;` ); 74 | 75 | } 76 | 77 | fields.push( name ); 78 | 79 | } 80 | 81 | 82 | return result.join( '\n' ); 83 | 84 | } ); 85 | 86 | lines.push( 87 | `return {\n${ 88 | fields 89 | .map( n => ` ${ n }` ) 90 | .join( ',\n' ) 91 | }\n};` 92 | ); 93 | console.log( lines.join( '\n\n' ) ); 94 | -------------------------------------------------------------------------------- /scripts/header.txt: -------------------------------------------------------------------------------- 1 | //DECLARE_BYTESWAP_DATADESC(); 2 | int id; 3 | int version; 4 | 5 | int checksum; // this has to be the same in the phy and vtx files to load! 6 | 7 | //inline const char * pszName(void) const { if (studiohdr2index && pStudioHdr2()->pszName()) return pStudioHdr2()->pszName(); else return name; } 8 | char name[64]; 9 | int length; 10 | 11 | 12 | Vector eyeposition; // ideal eye position 13 | 14 | Vector illumposition; // illumination center 15 | 16 | Vector hull_min; // ideal movement hull size 17 | Vector hull_max; 18 | 19 | Vector view_bbmin; // clipping bounding box 20 | Vector view_bbmax; 21 | 22 | int flags; 23 | 24 | int numbones; // bones 25 | int boneindex; 26 | 27 | int numbonecontrollers; // bone controllers 28 | int bonecontrollerindex; 29 | 30 | 31 | int numhitboxsets; 32 | int hitboxsetindex; 33 | 34 | 35 | // file local animations? and sequences 36 | //private: 37 | int numlocalanim; // animations/poses 38 | int localanimindex; // animation descriptions 39 | 40 | int numlocalseq; // sequences 41 | int localseqindex; 42 | 43 | //public: 44 | 45 | //private: 46 | mutable int activitylistversion; // initialization flag - have the sequences been indexed? 47 | mutable int eventsindexed; 48 | //public: 49 | 50 | // raw textures 51 | int numtextures; 52 | int textureindex; 53 | 54 | 55 | // raw textures search paths 56 | int numcdtextures; 57 | int cdtextureindex; 58 | inline char *pCdtexture(int i) const { return (((char *)this) + *((int *)(((byte *)this) + cdtextureindex) + i)); }; 59 | 60 | // replaceable textures tables 61 | int numskinref; 62 | int numskinfamilies; 63 | int skinindex; 64 | inline short *pSkinref(int i) const { return (short *)(((byte *)this) + skinindex) + i; }; 65 | 66 | int numbodyparts; 67 | int bodypartindex; 68 | 69 | // queryable attachable points 70 | //private: 71 | int numlocalattachments; 72 | int localattachmentindex; 73 | 74 | //public: 75 | 76 | // animation node to animation node transition graph 77 | //private: 78 | int numlocalnodes; 79 | int localnodeindex; 80 | int localnodenameindex; 81 | 82 | 83 | int numflexdesc; 84 | int flexdescindex; 85 | 86 | 87 | int numflexcontrollers; 88 | int flexcontrollerindex; 89 | 90 | 91 | int numflexrules; 92 | int flexruleindex; 93 | 94 | 95 | int numikchains; 96 | int ikchainindex; 97 | 98 | 99 | int nummouths; 100 | int mouthindex; 101 | 102 | 103 | //private: 104 | int numlocalposeparameters; 105 | int localposeparamindex; 106 | 107 | //public: 108 | 109 | int surfacepropindex; 110 | 111 | // Key values 112 | int keyvalueindex; 113 | int keyvaluesize; 114 | 115 | int numlocalikautoplaylocks; 116 | int localikautoplaylockindex; 117 | 118 | 119 | // The collision model mass that jay wanted 120 | float mass; 121 | int contents; 122 | 123 | // external animations, models, etc. 124 | int numincludemodels; 125 | int includemodelindex; 126 | 127 | 128 | // implementation specific back pointer to virtual data 129 | mutable void *virtualModel; 130 | 131 | 132 | // for demand loaded animation blocks 133 | int szanimblocknameindex; 134 | int numanimblocks; 135 | int animblockindex; 136 | 137 | mutable void *animblockModel; 138 | 139 | int bonetablebynameindex; 140 | 141 | // used by tools only that don't cache, but persist mdl's peer data 142 | // engine uses virtualModel to back link to cache pointers 143 | void *pVertexBase; 144 | void *pIndexBase; 145 | 146 | // if STUDIOHDR_FLAGS_CONSTANT_DIRECTIONAL_LIGHT_DOT is set, 147 | // this value is used to calculate directional components of lighting 148 | // on static props 149 | byte constdirectionallightdot; 150 | 151 | // set during load of mdl data to track *desired* lod configuration (not actual) 152 | // the *actual* clamped root lod is found in studiohwdata 153 | // this is stored here as a global store to ensure the staged loading matches the rendering 154 | byte rootLOD; 155 | 156 | // set in the mdl data to specify that lod configuration should only allow first numAllowRootLODs 157 | // to be set as root LOD: 158 | // numAllowedRootLODs = 0 means no restriction, any lod can be set as root lod. 159 | // numAllowedRootLODs = N means that lod0 - lod(N-1) can be set as root lod, but not lodN or lower. 160 | byte numAllowedRootLODs; 161 | 162 | byte unused[1]; 163 | 164 | int unused4; // zero out if version < 47 165 | 166 | int numflexcontrollerui; 167 | int flexcontrolleruiindex; 168 | 169 | 170 | float flVertAnimFixedPointScale; 171 | 172 | 173 | int unused3[1]; 174 | 175 | // FIXME: Remove when we up the model version. Move all fields of studiohdr2_t into studiohdr_t. 176 | int studiohdr2index; 177 | 178 | 179 | // Src bone transforms are transformations that will convert .dmx or .smd-based animations into .mdl-based animations 180 | 181 | 182 | 183 | 184 | 185 | // NOTE: No room to add stuff? Up the .mdl file format version 186 | // [and move all fields in studiohdr2_t into studiohdr_t and kill studiohdr2_t], 187 | // or add your stuff to studiohdr2_t. See NumSrcBoneTransforms/SrcBoneTransform for the pattern to use. 188 | int unused2[1]; -------------------------------------------------------------------------------- /src/MDLLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultLoadingManager, 3 | FileLoader, 4 | Vector3, 5 | Quaternion, 6 | Euler, 7 | Matrix4 8 | } from 'three'; 9 | 10 | // MDL: https://developer.valvesoftware.com/wiki/MDL 11 | // https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/public/studio.h 12 | 13 | const MDLLoader = function ( manager ) { 14 | 15 | this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; 16 | 17 | }; 18 | 19 | MDLLoader.prototype = { 20 | 21 | constructor: MDLLoader, 22 | 23 | load: function ( url, onLoad, onProgress, onError ) { 24 | 25 | const scope = this; 26 | 27 | const loader = new FileLoader( this.manager ); 28 | loader.setPath( this.path ); 29 | loader.setResponseType( 'arraybuffer' ); 30 | loader.load( url, function ( text ) { 31 | 32 | onLoad( scope.parse( text ) ); 33 | 34 | }, onProgress, onError ); 35 | 36 | }, 37 | 38 | parse: function ( buffer ) { 39 | 40 | function readString( dataView, offset, count = Infinity ) { 41 | 42 | let str = ''; 43 | for ( let j = 0; j < count; j ++ ) { 44 | 45 | const c = dataView.getUint8( j + offset ); 46 | if ( c === 0 ) break; 47 | 48 | str += String.fromCharCode( c ); 49 | 50 | } 51 | 52 | return str; 53 | 54 | } 55 | 56 | // studiohdr_t 57 | function parseHeader( buffer ) { 58 | 59 | const dataView = new DataView( buffer ); 60 | let i = 0; 61 | 62 | // int id; 63 | const id = dataView.getInt32( i, true ); 64 | i += 4; 65 | 66 | // int version; 67 | const version = dataView.getInt32( i, true ); 68 | i += 4; 69 | 70 | // int checksum; 71 | const checksum = dataView.getInt32( i, true ); 72 | i += 4; 73 | 74 | // char name[64]; 75 | const name = readString( dataView, i, 64 ); 76 | i += 64; 77 | 78 | // int length; 79 | const length = dataView.getInt32( i, true ); 80 | i += 4; 81 | 82 | // Vector eyeposition; 83 | const eyeposition = new Vector3(); 84 | eyeposition.x = dataView.getFloat32( i + 0, true ); 85 | eyeposition.y = dataView.getFloat32( i + 4, true ); 86 | eyeposition.z = dataView.getFloat32( i + 8, true ); 87 | i += 12; 88 | 89 | // Vector illumposition; 90 | const illumposition = new Vector3(); 91 | illumposition.x = dataView.getFloat32( i + 0, true ); 92 | illumposition.y = dataView.getFloat32( i + 4, true ); 93 | illumposition.z = dataView.getFloat32( i + 8, true ); 94 | i += 12; 95 | 96 | // Vector hull_min; 97 | const hullMin = new Vector3(); 98 | hullMin.x = dataView.getFloat32( i + 0, true ); 99 | hullMin.y = dataView.getFloat32( i + 4, true ); 100 | hullMin.z = dataView.getFloat32( i + 8, true ); 101 | i += 12; 102 | 103 | // Vector hull_max; 104 | const hullMax = new Vector3(); 105 | hullMax.x = dataView.getFloat32( i + 0, true ); 106 | hullMax.y = dataView.getFloat32( i + 4, true ); 107 | hullMax.z = dataView.getFloat32( i + 8, true ); 108 | i += 12; 109 | 110 | // Vector view_bbmin; 111 | const viewBbmin = new Vector3(); 112 | viewBbmin.x = dataView.getFloat32( i + 0, true ); 113 | viewBbmin.y = dataView.getFloat32( i + 4, true ); 114 | viewBbmin.z = dataView.getFloat32( i + 8, true ); 115 | i += 12; 116 | 117 | // Vector view_bbmax; 118 | const viewBbmax = new Vector3(); 119 | viewBbmax.x = dataView.getFloat32( i + 0, true ); 120 | viewBbmax.y = dataView.getFloat32( i + 4, true ); 121 | viewBbmax.z = dataView.getFloat32( i + 8, true ); 122 | i += 12; 123 | 124 | // int flags; 125 | const flags = dataView.getInt32( i, true ); 126 | i += 4; 127 | 128 | // int numbones; 129 | const numbones = dataView.getInt32( i, true ); 130 | i += 4; 131 | 132 | // int boneindex; 133 | const boneindex = dataView.getInt32( i, true ); 134 | i += 4; 135 | 136 | // int numbonecontrollers; 137 | const numbonecontrollers = dataView.getInt32( i, true ); 138 | i += 4; 139 | 140 | // int bonecontrollerindex; 141 | const bonecontrollerindex = dataView.getInt32( i, true ); 142 | i += 4; 143 | 144 | // int numhitboxsets; 145 | const numhitboxsets = dataView.getInt32( i, true ); 146 | i += 4; 147 | 148 | // int hitboxsetindex; 149 | const hitboxsetindex = dataView.getInt32( i, true ); 150 | i += 4; 151 | 152 | // int numlocalanim; 153 | const numlocalanim = dataView.getInt32( i, true ); 154 | i += 4; 155 | 156 | // int localanimindex; 157 | const localanimindex = dataView.getInt32( i, true ); 158 | i += 4; 159 | 160 | // int numlocalseq; 161 | const numlocalseq = dataView.getInt32( i, true ); 162 | i += 4; 163 | 164 | // int localseqindex; 165 | const localseqindex = dataView.getInt32( i, true ); 166 | i += 4; 167 | 168 | // mutable int activitylistversion; 169 | const activitylistversion = dataView.getInt32( i, true ); 170 | i += 4; 171 | 172 | // mutable int eventsindexed; 173 | const eventsindexed = dataView.getInt32( i, true ); 174 | i += 4; 175 | 176 | // int numtextures; 177 | const numtextures = dataView.getInt32( i, true ); 178 | i += 4; 179 | 180 | // int textureindex; 181 | const textureindex = dataView.getInt32( i, true ); 182 | i += 4; 183 | 184 | // int numcdtextures; 185 | const numcdtextures = dataView.getInt32( i, true ); 186 | i += 4; 187 | 188 | // int cdtextureindex; 189 | const cdtextureindex = dataView.getInt32( i, true ); 190 | i += 4; 191 | 192 | // int numskinref; 193 | const numskinref = dataView.getInt32( i, true ); 194 | i += 4; 195 | 196 | // int numskinfamilies; 197 | const numskinfamilies = dataView.getInt32( i, true ); 198 | i += 4; 199 | 200 | // int skinindex; 201 | const skinindex = dataView.getInt32( i, true ); 202 | i += 4; 203 | 204 | // int numbodyparts; 205 | const numbodyparts = dataView.getInt32( i, true ); 206 | i += 4; 207 | 208 | // int bodypartindex; 209 | const bodypartindex = dataView.getInt32( i, true ); 210 | i += 4; 211 | 212 | // int numlocalattachments; 213 | const numlocalattachments = dataView.getInt32( i, true ); 214 | i += 4; 215 | 216 | // int localattachmentindex; 217 | const localattachmentindex = dataView.getInt32( i, true ); 218 | i += 4; 219 | 220 | // int numlocalnodes; 221 | const numlocalnodes = dataView.getInt32( i, true ); 222 | i += 4; 223 | 224 | // int localnodeindex; 225 | const localnodeindex = dataView.getInt32( i, true ); 226 | i += 4; 227 | 228 | // int localnodenameindex; 229 | const localnodenameindex = dataView.getInt32( i, true ); 230 | i += 4; 231 | 232 | // int numflexdesc; 233 | const numflexdesc = dataView.getInt32( i, true ); 234 | i += 4; 235 | 236 | // int flexdescindex; 237 | const flexdescindex = dataView.getInt32( i, true ); 238 | i += 4; 239 | 240 | // int numflexcontrollers; 241 | const numflexcontrollers = dataView.getInt32( i, true ); 242 | i += 4; 243 | 244 | // int flexcontrollerindex; 245 | const flexcontrollerindex = dataView.getInt32( i, true ); 246 | i += 4; 247 | 248 | // int numflexrules; 249 | const numflexrules = dataView.getInt32( i, true ); 250 | i += 4; 251 | 252 | // int flexruleindex; 253 | const flexruleindex = dataView.getInt32( i, true ); 254 | i += 4; 255 | 256 | // int numikchains; 257 | const numikchains = dataView.getInt32( i, true ); 258 | i += 4; 259 | 260 | // int ikchainindex; 261 | const ikchainindex = dataView.getInt32( i, true ); 262 | i += 4; 263 | 264 | // int nummouths; 265 | const nummouths = dataView.getInt32( i, true ); 266 | i += 4; 267 | 268 | // int mouthindex; 269 | const mouthindex = dataView.getInt32( i, true ); 270 | i += 4; 271 | 272 | // int numlocalposeparameters; 273 | const numlocalposeparameters = dataView.getInt32( i, true ); 274 | i += 4; 275 | 276 | // int localposeparamindex; 277 | const localposeparamindex = dataView.getInt32( i, true ); 278 | i += 4; 279 | 280 | // int surfacepropindex; 281 | const surfacepropindex = dataView.getInt32( i, true ); 282 | i += 4; 283 | 284 | // int keyvalueindex; 285 | const keyvalueindex = dataView.getInt32( i, true ); 286 | i += 4; 287 | 288 | // int keyvaluesize; 289 | const keyvaluesize = dataView.getInt32( i, true ); 290 | i += 4; 291 | 292 | // int numlocalikautoplaylocks; 293 | const numlocalikautoplaylocks = dataView.getInt32( i, true ); 294 | i += 4; 295 | 296 | // int localikautoplaylockindex; 297 | const localikautoplaylockindex = dataView.getInt32( i, true ); 298 | i += 4; 299 | 300 | // float mass; 301 | const mass = dataView.getFloat32( i, true ); 302 | i += 4; 303 | 304 | // int contents; 305 | const contents = dataView.getInt32( i, true ); 306 | i += 4; 307 | 308 | // int numincludemodels; 309 | const numincludemodels = dataView.getInt32( i, true ); 310 | i += 4; 311 | 312 | // int includemodelindex; 313 | const includemodelindex = dataView.getInt32( i, true ); 314 | i += 4; 315 | 316 | // mutable void *virtualModel; 317 | i += 4; 318 | 319 | // int szanimblocknameindex; 320 | const szanimblocknameindex = dataView.getInt32( i, true ); 321 | i += 4; 322 | 323 | // int numanimblocks; 324 | const numanimblocks = dataView.getInt32( i, true ); 325 | i += 4; 326 | 327 | // int animblockindex; 328 | const animblockindex = dataView.getInt32( i, true ); 329 | i += 4; 330 | 331 | // TODO: load anim blocks 332 | 333 | // mutable void *animblockModel; 334 | i += 4; 335 | 336 | // int bonetablebynameindex; 337 | const bonetablebynameindex = dataView.getInt32( i, true ); 338 | i += 4; 339 | 340 | // void *pVertexBase; 341 | i += 4; 342 | 343 | // void *pIndexBase; 344 | i += 4; 345 | 346 | // byte constdirectionallightdot; 347 | const constdirectionallightdot = dataView.getUint8( i, true ); 348 | i += 1; 349 | 350 | // byte rootLOD; 351 | const rootLOD = dataView.getUint8( i, true ); 352 | i += 1; 353 | 354 | // byte numAllowedRootLODs; 355 | const numAllowedRootLODs = dataView.getUint8( i, true ); 356 | i += 1; 357 | 358 | // byte unused[1]; 359 | i += 1; 360 | 361 | // int unused4; 362 | i += 4; 363 | 364 | // int numflexcontrollerui; 365 | const numflexcontrollerui = dataView.getInt32( i, true ); 366 | i += 4; 367 | 368 | // int flexcontrolleruiindex; 369 | const flexcontrolleruiindex = dataView.getInt32( i, true ); 370 | i += 4; 371 | 372 | // float flVertAnimFixedPointScale; 373 | const flVertAnimFixedPointScale = dataView.getFloat32( i, true ); 374 | i += 4; 375 | 376 | // int unused3[1]; 377 | i += 4; 378 | 379 | // int studiohdr2index; 380 | const studiohdr2index = dataView.getInt32( i, true ); 381 | i += 4; 382 | 383 | // int unused2[1]; 384 | i += 4; 385 | 386 | return { 387 | id, 388 | version, 389 | checksum, 390 | name, 391 | length, 392 | eyeposition, 393 | illumposition, 394 | hullMin, 395 | hullMax, 396 | viewBbmin, 397 | viewBbmax, 398 | flags, 399 | numbones, 400 | boneindex, 401 | numbonecontrollers, 402 | bonecontrollerindex, 403 | numhitboxsets, 404 | hitboxsetindex, 405 | numlocalanim, 406 | localanimindex, 407 | numlocalseq, 408 | localseqindex, 409 | activitylistversion, 410 | eventsindexed, 411 | numtextures, 412 | textureindex, 413 | numcdtextures, 414 | cdtextureindex, 415 | numskinref, 416 | numskinfamilies, 417 | skinindex, 418 | numbodyparts, 419 | bodypartindex, 420 | numlocalattachments, 421 | localattachmentindex, 422 | numlocalnodes, 423 | localnodeindex, 424 | localnodenameindex, 425 | numflexdesc, 426 | flexdescindex, 427 | numflexcontrollers, 428 | flexcontrollerindex, 429 | numflexrules, 430 | flexruleindex, 431 | numikchains, 432 | ikchainindex, 433 | nummouths, 434 | mouthindex, 435 | numlocalposeparameters, 436 | localposeparamindex, 437 | surfacepropindex, 438 | keyvalueindex, 439 | keyvaluesize, 440 | numlocalikautoplaylocks, 441 | localikautoplaylockindex, 442 | mass, 443 | contents, 444 | numincludemodels, 445 | includemodelindex, 446 | // virtualModel, 447 | szanimblocknameindex, 448 | numanimblocks, 449 | animblockindex, 450 | // animblockModel, 451 | bonetablebynameindex, 452 | // pVertexBase, 453 | // pIndexBase, 454 | constdirectionallightdot, 455 | rootLOD, 456 | numAllowedRootLODs, 457 | // unused, 458 | // unused4, 459 | numflexcontrollerui, 460 | flexcontrolleruiindex, 461 | flVertAnimFixedPointScale, 462 | // unused3, 463 | studiohdr2index, 464 | // unused2 465 | }; 466 | 467 | } 468 | 469 | function parseSecondaryHeader( buffer, offset ) { 470 | 471 | if ( offset === 0 ) return null; 472 | 473 | const dataView = new DataView( buffer ); 474 | let i = offset; 475 | 476 | // int 477 | const srcbonetransformCount = dataView.getInt32( i, true ); 478 | i += 4; 479 | 480 | // int 481 | const srcbonetransformIndex = dataView.getInt32( i, true ); 482 | i += 4; 483 | 484 | // int 485 | const illumpositionattachmentindex = dataView.getInt32( i, true ); 486 | i += 4; 487 | 488 | // float 489 | const flMaxEyeDeflection = dataView.getFloat32( i, true ); 490 | i += 4; 491 | 492 | // int 493 | const linearboneIndex = dataView.getInt32( i, true ); 494 | i += 4; 495 | 496 | // int[64] 497 | const unknown = null; 498 | i += 64 * 4; 499 | 500 | return { 501 | srcbonetransformCount, 502 | srcbonetransformIndex, 503 | illumpositionattachmentindex, 504 | flMaxEyeDeflection, 505 | linearboneIndex, 506 | unknown 507 | }; 508 | 509 | 510 | } 511 | 512 | function readData( header, header2, buffer ) { 513 | 514 | const dataView = new DataView( buffer ); 515 | const textures = []; 516 | // struct mstudiotexture_t 517 | for ( let i = 0; i < header.numtextures; i ++ ) { 518 | 519 | const offset = header.textureindex + i * 16 * 4; 520 | const sznameindex = dataView.getInt32( offset, true ); 521 | // var flags = dataView.getInt32( offset + 4, true ); 522 | // var used = dataView.getInt32( offset + 8, true ); 523 | 524 | // int unused1 525 | // void* material 526 | // void* clientmaterial 527 | // int unused[10] 528 | 529 | textures.push( readString( dataView, offset + sznameindex ) ); 530 | 531 | } 532 | 533 | const textureDirectories = []; 534 | for ( let i = 0; i < header.numcdtextures; i ++ ) { 535 | 536 | const offset = header.cdtextureindex + i * 4; 537 | const ptr = dataView.getInt32( offset, true ); 538 | textureDirectories.push( readString( dataView, ptr ) ); 539 | 540 | } 541 | 542 | const includeModels = []; 543 | // struct mstudiomodelgroup_t 544 | for ( let i = 0; i < header.numincludemodels; i ++ ) { 545 | 546 | const offset = header.includemodelindex + i * 8; 547 | const model = {}; 548 | model.label = readString( dataView, offset + dataView.getInt32( offset + 0, true ) ); 549 | model.name = readString( dataView, offset + dataView.getInt32( offset + 4, true ) ); 550 | includeModels.push( model ); 551 | 552 | } 553 | 554 | // struct mstudiobodyparts_t 555 | const bodyParts = []; 556 | for ( let i = 0; i < header.numbodyparts; i ++ ) { 557 | 558 | const offset = header.bodypartindex + i * 16; 559 | const bodyPart = {}; 560 | bodyPart.name = readString( dataView, offset + dataView.getInt32( offset + 0, true ) ); 561 | bodyPart.nummodels = dataView.getInt32( offset + 4, true ); 562 | bodyPart.base = dataView.getInt32( offset + 8, true ); 563 | bodyPart.modelindex = dataView.getInt32( offset + 12, true ); 564 | bodyPart.models = []; 565 | bodyParts.push( bodyPart ); 566 | 567 | // struct mstudiomodel_t 568 | for ( let i2 = 0; i2 < bodyPart.nummodels; i2 ++ ) { 569 | 570 | const offset2 = offset + bodyPart.modelindex + i2 * 148; 571 | const model = {}; 572 | model.name = readString( dataView, offset2 + 0, 64 ); 573 | model.type = dataView.getInt32( offset2 + 64, true ); 574 | model.boundingradius = dataView.getFloat32( offset2 + 64 + 4, true ); 575 | 576 | model.nummeshes = dataView.getInt32( offset2 + 64 + 8, true ); 577 | model.meshindex = dataView.getInt32( offset2 + 64 + 12, true ); 578 | 579 | model.numvertices = dataView.getInt32( offset2 + 64 + 16, true ); 580 | model.vertexindex = dataView.getInt32( offset2 + 64 + 20, true ); 581 | model.tangentsindex = dataView.getInt32( offset2 + 64 + 24, true ); 582 | 583 | model.numattachments = dataView.getInt32( offset2 + 64 + 28, true ); 584 | model.attachmentindex = dataView.getInt32( offset2 + 64 + 32, true ); 585 | model.numeyeballs = dataView.getInt32( offset2 + 64 + 36, true ); 586 | model.eyeballindex = dataView.getInt32( offset2 + 64 + 40, true ); 587 | 588 | // 108 bytes so far 589 | 590 | // mstudio_modelvertexdata_t -- contains two void pointers 591 | 592 | // int unused[8] 593 | 594 | // 108 + 8 + 8 * 4 = 148 bytes 595 | 596 | model.meshes = []; 597 | bodyPart.models.push( model ); 598 | 599 | // TODO: Some times the amount of meshes here is (seemingly incorrectly) huge 600 | // and causes an out of memory crash 601 | 602 | // struct mstudiomesh_t 603 | for ( let i3 = 0; i3 < model.nummeshes; i3 ++ ) { 604 | 605 | const offset3 = offset2 + model.meshindex + i3 * 116; 606 | const mesh = {}; 607 | mesh.material = dataView.getInt32( offset3 + 0, true ); 608 | mesh.modelindex = dataView.getInt32( offset3 + 4, true ); 609 | 610 | mesh.numvertices = dataView.getInt32( offset3 + 8, true ); 611 | mesh.vertexoffset = dataView.getInt32( offset3 + 12, true ); 612 | 613 | mesh.numflexes = dataView.getInt32( offset3 + 16, true ); 614 | mesh.flexindex = dataView.getInt32( offset3 + 20, true ); 615 | 616 | mesh.materialtype = dataView.getInt32( offset3 + 24, true ); 617 | mesh.materialparam = dataView.getInt32( offset3 + 28, true ); 618 | 619 | mesh.meshid = dataView.getInt32( offset3 + 32, true ); 620 | mesh.center = new Vector3( 621 | dataView.getFloat32( offset3 + 36, true ), 622 | dataView.getFloat32( offset3 + 40, true ), 623 | dataView.getFloat32( offset3 + 44, true ), 624 | ); 625 | 626 | // 48 bytes total 627 | 628 | // TODO: should we parse this? 629 | // mstudio_modelvertexdata_t vertexdata (36 bytes) 630 | // mstudio_modelvertexdata_t *modelvertexdata; -- 4 631 | // int numLODVertexes[MAX_NUM_LODS]; -- 4 * 8 632 | 633 | // 84 bytes total 634 | 635 | // int unused[8] 636 | 637 | // 116 total 638 | 639 | model.meshes.push( mesh ); 640 | 641 | } 642 | 643 | } 644 | 645 | } 646 | 647 | // mstudiobone_t 648 | const bones = []; 649 | for ( let i = 0; i < header.numbones; i ++ ) { 650 | 651 | const offset = header.boneindex + i * 216; 652 | const bone = {}; 653 | 654 | bone.name = readString( dataView, offset + dataView.getInt32( offset + 0, true ) ); 655 | bone.parent = dataView.getInt32( offset + 4, true ); 656 | 657 | const bonecontroller = new Array( 6 ); 658 | for ( let i = 0; i < 6; i ++ ) { 659 | 660 | bonecontroller[ i ] = dataView.getInt32( offset + 8 + i * 4, true ); 661 | 662 | } 663 | 664 | bone.bonecontroller = bonecontroller; 665 | 666 | // 6 * 4 = 24 667 | // 8 + 24 = 32 668 | 669 | bone.pos = new Vector3(); 670 | bone.pos.x = dataView.getFloat32( offset + 32, true ); 671 | bone.pos.y = dataView.getFloat32( offset + 36, true ); 672 | bone.pos.z = dataView.getFloat32( offset + 40, true ); 673 | 674 | bone.quaternion = new Quaternion(); 675 | bone.quaternion.x = dataView.getFloat32( offset + 44, true ); 676 | bone.quaternion.y = dataView.getFloat32( offset + 48, true ); 677 | bone.quaternion.z = dataView.getFloat32( offset + 52, true ); 678 | bone.quaternion.w = dataView.getFloat32( offset + 56, true ); 679 | 680 | bone.radianEuler = new Euler(); 681 | bone.radianEuler.x = dataView.getFloat32( offset + 60, true ); 682 | bone.radianEuler.y = dataView.getFloat32( offset + 64, true ); 683 | bone.radianEuler.z = dataView.getFloat32( offset + 68, true ); 684 | 685 | bone.posscale = new Vector3(); 686 | bone.posscale.x = dataView.getFloat32( offset + 72, true ); 687 | bone.posscale.y = dataView.getFloat32( offset + 76, true ); 688 | bone.posscale.z = dataView.getFloat32( offset + 80, true ); 689 | 690 | bone.rotscale = new Vector3(); 691 | bone.rotscale.x = dataView.getFloat32( offset + 84, true ); 692 | bone.rotscale.y = dataView.getFloat32( offset + 88, true ); 693 | bone.rotscale.z = dataView.getFloat32( offset + 92, true ); 694 | 695 | const posToBone = new Matrix4(); 696 | posToBone.identity(); 697 | for ( let i = 0; i < 12; i ++ ) { 698 | 699 | posToBone.elements[ i ] = dataView.getFloat32( offset + 96 + i * 4, true ); 700 | 701 | } 702 | 703 | bone.posToBone = posToBone; 704 | // console.log( posToBone.elements ) 705 | 706 | // postobone 707 | // 3 * 4 * 4 bytes = 48 708 | // 96 + 48 = 144 709 | 710 | bone.qAlignment = new Quaternion(); 711 | bone.qAlignment.x = dataView.getFloat32( offset + 144, true ); 712 | bone.qAlignment.y = dataView.getFloat32( offset + 148, true ); 713 | bone.qAlignment.z = dataView.getFloat32( offset + 152, true ); 714 | bone.qAlignment.w = dataView.getFloat32( offset + 156, true ); 715 | 716 | bone.flags = dataView.getInt32( offset + 160, true ); 717 | bone.proctype = dataView.getInt32( offset + 164, true ); 718 | bone.procindex = dataView.getInt32( offset + 168, true ); 719 | bone.physicsbone = dataView.getInt32( offset + 172, true ); 720 | bone.surfacepropidx = dataView.getInt32( offset + 176, true ); 721 | bone.contents = dataView.getInt32( offset + 180, true ); 722 | 723 | // unused 724 | // 4 * 8 bytes = 32 725 | // 184 + 32 = 216 726 | 727 | bones.push( bone ); 728 | 729 | } 730 | 731 | const boneControllers = []; 732 | for ( let i = 0; i < header.numbonecontrollers; i ++ ) { 733 | 734 | const boneController = {}; 735 | const offset = header.bonecontrollerindex + i * 56; 736 | 737 | boneController.bone = dataView.getInt32( offset, true ); 738 | boneController.type = dataView.getInt32( offset + 4, true ); // X Y Z XR YR ZR M 739 | boneController.start = dataView.getFloat32( offset + 8, true ); 740 | boneController.end = dataView.getFloat32( offset + 12, true ); 741 | boneController.rest = dataView.getInt32( offset + 16, true ); 742 | boneController.inputfield = dataView.getInt32( offset + 20, true ); 743 | 744 | // int unused[8] 745 | 746 | boneControllers.push( boneController ); 747 | 748 | } 749 | 750 | const animDescriptions = []; 751 | // struct mstudioanimdesc_t 752 | for ( let i = 0; i < header.numlocalanim; i ++ ) { 753 | 754 | const animDesc = {}; 755 | const offset = header.localanimindex + i * 100; 756 | 757 | animDesc.baseptr = dataView.getInt32( offset, true ); 758 | animDesc.name = readString( dataView, offset + dataView.getInt32( offset + 4, true ) ); 759 | animDesc.fps = dataView.getFloat32( offset + 8, true ); 760 | animDesc.flags = dataView.getInt32( offset + 12, true ); 761 | animDesc.numframes = dataView.getInt32( offset + 16, true ); 762 | 763 | animDesc.nummovements = dataView.getInt32( offset + 20, true ); 764 | animDesc.movementindex = dataView.getInt32( offset + 24, true ); 765 | 766 | // struct mstudiomovement_t 767 | animDesc.movements = []; 768 | for ( let i2 = 0; i2 < animDesc.nummovements; i2 ++ ) { 769 | 770 | const movement = {}; 771 | const offset2 = offset + animDesc.movementindex + i2 * 44; 772 | 773 | movement.endframe = dataView.getInt32( offset2, true ); 774 | movement.motionflags = dataView.getInt32( offset2 + 4, true ); 775 | movement.v0 = dataView.getFloat32( offset2 + 8, true ); 776 | movement.v1 = dataView.getFloat32( offset2 + 12, true ); 777 | movement.angle = dataView.getFloat32( offset2 + 16, true ); 778 | 779 | movement.vector = {}; 780 | movement.vector.x = dataView.getFloat32( offset2 + 20, true ); 781 | movement.vector.y = dataView.getFloat32( offset2 + 24, true ); 782 | movement.vector.z = dataView.getFloat32( offset2 + 28, true ); 783 | 784 | movement.position = {}; 785 | movement.position.x = dataView.getFloat32( offset2 + 32, true ); 786 | movement.position.y = dataView.getFloat32( offset2 + 36, true ); 787 | movement.position.z = dataView.getFloat32( offset2 + 40, true ); 788 | 789 | animDesc.movements.push( movement ); 790 | 791 | } 792 | 793 | // int unused[16] 794 | 795 | // 6 * 4 + 28 = 52 bytes 796 | 797 | animDesc.animblock = dataView.getInt32( offset + 52, true ); 798 | animDesc.animindex = dataView.getInt32( offset + 56, true ); 799 | 800 | // TODO: Load anim blocks 801 | 802 | animDesc.numikrules = dataView.getInt32( offset + 60, true ); 803 | animDesc.ikruleindex = dataView.getInt32( offset + 64, true ); 804 | animDesc.animblockikruleindex = dataView.getInt32( offset + 68, true ); 805 | 806 | // TODO: Load IK Rules 807 | 808 | animDesc.numlocalhierarchy = dataView.getInt32( offset + 72, true ); 809 | animDesc.numlocalhierarchyindex = dataView.getInt32( offset + 76, true ); 810 | 811 | // TODO: Load local hierarchies 812 | 813 | animDesc.sectionindex = dataView.getInt32( offset + 80, true ); 814 | animDesc.sectionframes = dataView.getInt32( offset + 84, true ); 815 | 816 | // TODO: Load animation sections 817 | 818 | animDesc.zeroframespan = dataView.getInt16( offset + 88, true ); 819 | animDesc.zeroframecount = dataView.getInt16( offset + 80, true ); 820 | animDesc.zeroframeindex = dataView.getInt32( offset + 92, true ); 821 | 822 | // TODO: Load zero frame data 823 | 824 | animDesc.zeroframestalltime = dataView.getFloat32( offset + 96, true ); 825 | 826 | animDescriptions.push( animDesc ); 827 | 828 | } 829 | 830 | // struct mstudioseqdesc_t 831 | const localSequences = []; 832 | for ( let i = 0; i < header.numlocalseq; i ++ ) { 833 | 834 | const localSeq = {}; 835 | const offset = header.localseqindex + i * 212; 836 | 837 | localSeq.baseptr = dataView.getInt32( offset, true ); 838 | localSeq.label = readString( dataView, offset + dataView.getInt32( offset + 4, true ) ); 839 | localSeq.activityName = readString( dataView, offset + dataView.getInt32( offset + 8, true ) ); 840 | 841 | localSeq.flags = dataView.getInt32( offset + 12, true ); 842 | localSeq.activity = dataView.getInt32( offset + 16, true ); 843 | localSeq.actweight = dataView.getInt32( offset + 20, true ); 844 | 845 | localSeq.numevents = dataView.getInt32( offset + 24, true ); 846 | localSeq.eventindex = dataView.getInt32( offset + 28, true ); 847 | 848 | // TODO: Load mstudioevent_t 849 | 850 | localSeq.bbmin = {}; 851 | localSeq.bbmin.x = dataView.getFloat32( offset + 32, true ); 852 | localSeq.bbmin.y = dataView.getFloat32( offset + 36, true ); 853 | localSeq.bbmin.z = dataView.getFloat32( offset + 40, true ); 854 | 855 | localSeq.bbmax = {}; 856 | localSeq.bbmax.x = dataView.getFloat32( offset + 44, true ); 857 | localSeq.bbmax.y = dataView.getFloat32( offset + 48, true ); 858 | localSeq.bbmax.z = dataView.getFloat32( offset + 52, true ); 859 | 860 | localSeq.numblends = dataView.getInt32( offset + 56, true ); 861 | localSeq.animindexindex = dataView.getInt32( offset + 60, true ); 862 | 863 | // TODO: Load inline anim(x, y) structs 864 | 865 | localSeq.movementindex = dataView.getInt32( offset + 64, true ); 866 | localSeq.groupsize = new Array( 2 ); 867 | localSeq.groupsize[ 0 ] = dataView.getInt32( offset + 68, true ); 868 | localSeq.groupsize[ 1 ] = dataView.getInt32( offset + 72, true ); 869 | 870 | localSeq.paramindex = new Array( 2 ); // X, Y, Z, XR, YR, ZR 871 | localSeq.paramindex[ 0 ] = dataView.getInt32( offset + 76, true ); 872 | localSeq.paramindex[ 1 ] = dataView.getInt32( offset + 80, true ); 873 | 874 | localSeq.paramstart = new Array( 2 ); 875 | localSeq.paramstart[ 0 ] = dataView.getFloat32( offset + 84, true ); 876 | localSeq.paramstart[ 1 ] = dataView.getFloat32( offset + 88, true ); 877 | 878 | localSeq.paramend = new Array( 2 ); 879 | localSeq.paramend[ 0 ] = dataView.getFloat32( offset + 92, true ); 880 | localSeq.paramend[ 1 ] = dataView.getFloat32( offset + 96, true ); 881 | 882 | localSeq.paramparent = dataView.getInt32( offset + 100, true ); 883 | 884 | localSeq.fadeintime = dataView.getFloat32( offset + 104, true ); 885 | localSeq.fadeouttime = dataView.getFloat32( offset + 108, true ); 886 | 887 | localSeq.localentrynode = dataView.getInt32( offset + 112, true ); 888 | localSeq.localexitynode = dataView.getInt32( offset + 116, true ); 889 | localSeq.nodeflags = dataView.getInt32( offset + 120, true ); 890 | 891 | localSeq.entryphase = dataView.getFloat32( offset + 124, true ); 892 | localSeq.exitphase = dataView.getFloat32( offset + 128, true ); 893 | 894 | localSeq.lastframe = dataView.getFloat32( offset + 132, true ); 895 | 896 | localSeq.nextseq = dataView.getInt32( offset + 136, true ); 897 | localSeq.pose = dataView.getInt32( offset + 140, true ); 898 | 899 | localSeq.numikrules = dataView.getInt32( offset + 144, true ); 900 | 901 | localSeq.numautolayers = dataView.getInt32( offset + 148, true ); 902 | localSeq.autolayerindex = dataView.getInt32( offset + 152, true ); 903 | 904 | // TODO: Load autolayers 905 | 906 | localSeq.weightlistindex = dataView.getInt32( offset + 156, true ); 907 | 908 | // TODO: Load weights 909 | 910 | localSeq.posekeyindex = dataView.getInt32( offset + 160, true ); 911 | 912 | // TODO: load pose keys 913 | 914 | localSeq.numiklocks = dataView.getInt32( offset + 164, true ); 915 | localSeq.iklockindex = dataView.getInt32( offset + 168, true ); 916 | 917 | // TODO: Load ik locks 918 | 919 | localSeq.keyvalueindex = dataView.getInt32( offset + 172, true ); 920 | localSeq.keyvaluesize = dataView.getInt32( offset + 176, true ); 921 | 922 | // TODO: Load key values 923 | 924 | localSeq.cycleposeindex = dataView.getInt32( offset + 180, true ); 925 | 926 | // int unused[7] 927 | 928 | // 7 * 4 + 184 = 212 bytes 929 | 930 | localSequences.push( localSeq ); 931 | 932 | } 933 | 934 | // mstudioikchain_t 935 | const ikchains = []; 936 | for ( let i = 0; i < header.numikchains; i ++ ) { 937 | 938 | const offset = header.ikchainindex + i * 16; 939 | 940 | const ikchain = {}; 941 | ikchain.name = readString( dataView, offset + dataView.getInt32( offset, true ) ); 942 | ikchain.linktype = dataView.getInt32( offset + 4, true ); 943 | ikchain.numlinks = dataView.getInt32( offset + 8, true ); 944 | ikchain.linkindex = dataView.getInt32( offset + 12, true ); 945 | ikchain.links = []; 946 | 947 | for ( let j = 0; j < ikchain.numlinks; j ++ ) { 948 | 949 | const linkOffset = offset + ikchain.linkindex + j * 28; 950 | 951 | const link = {}; 952 | link.bone = dataView.getInt32( linkOffset, true ); 953 | link.kneeDir = {}; 954 | link.kneeDir.x = dataView.getFloat32( linkOffset + 4, true ); 955 | link.kneeDir.y = dataView.getFloat32( linkOffset + 8, true ); 956 | link.kneeDir.z = dataView.getFloat32( linkOffset + 12, true ); 957 | ikchain.links.push( link ); 958 | 959 | } 960 | 961 | ikchains.push( ikchain ); 962 | 963 | } 964 | 965 | const skinsTable = []; 966 | for ( let i = 0; i < header.numskinfamilies; i ++ ) { 967 | 968 | const offset = header.skinindex + i * header.numskinref * 2; 969 | const ref = []; 970 | for ( let j = 0; j < header.numskinref; j ++ ) { 971 | 972 | ref.push( dataView.getUint16( offset + j * 2, true ) ); 973 | 974 | } 975 | 976 | skinsTable.push( ref ); 977 | 978 | } 979 | 980 | const surfaceProp = readString( dataView, header.surfacepropindex ); 981 | 982 | return { 983 | textures, 984 | textureDirectories, 985 | includeModels, 986 | surfaceProp, 987 | bodyParts, 988 | bones, 989 | boneControllers, 990 | animDescriptions, 991 | localSequences, 992 | skinsTable, 993 | ikchains, 994 | }; 995 | 996 | } 997 | 998 | const header = parseHeader( buffer ); 999 | const header2 = parseSecondaryHeader( buffer, header.studiohdr2index ); 1000 | return Object.assign( { header, header2, buffer }, readData( header, header2, buffer ) ); 1001 | 1002 | } 1003 | 1004 | }; 1005 | 1006 | export { MDLLoader }; 1007 | -------------------------------------------------------------------------------- /src/SourceModelLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultLoadingManager, 3 | BufferAttribute, 4 | Group, 5 | Bone, 6 | Skeleton, 7 | BufferGeometry, 8 | SkinnedMesh, 9 | TriangleStripDrawMode, 10 | MeshPhongMaterial, 11 | } from 'three'; 12 | import { MDLLoader } from './MDLLoader.js'; 13 | import { VMTLoader } from './VMTLoader.js'; 14 | import { VTXLoader } from './VTXLoader.js'; 15 | import { VVDLoader } from './VVDLoader.js'; 16 | import { toTrianglesDrawMode } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; 17 | 18 | class SourceModelLoader { 19 | 20 | constructor( manager ) { 21 | 22 | this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; 23 | 24 | } 25 | 26 | load( url, onLoad, onProgress, onError ) { 27 | 28 | function reverseInPlace( array ) { 29 | 30 | const halfLen = array.length / 2; 31 | for ( let i = 0, i2 = array.length - 1; i < halfLen; i ++, i2 -- ) { 32 | 33 | const tmp = array[ i ]; 34 | array[ i ] = array[ i2 ]; 35 | array[ i2 ] = tmp; 36 | 37 | } 38 | 39 | return array; 40 | 41 | } 42 | 43 | function toGeometryIndex( vtxBuffer, model, mesh, stripGroup, strip ) { 44 | 45 | const vtxDataView = new DataView( vtxBuffer ); 46 | const indexArray = new Uint16Array( strip.numIndices ); 47 | 48 | for ( let i = 0, l = strip.numIndices; i < l; i ++ ) { 49 | 50 | const index = strip.indexOffset + i; 51 | const index2 = vtxDataView.getUint16( stripGroup.indexDataStart + index * 2, true ); 52 | const index3 = vtxDataView.getUint16( stripGroup.vertexDataStart + index2 * 9 + 4, true ); 53 | const index4 = mesh.vertexoffset + index3; 54 | const index5 = index4 + model.vertexindex / 48; 55 | 56 | indexArray[ i ] = index5; 57 | 58 | } 59 | 60 | reverseInPlace( indexArray ); 61 | 62 | return new BufferAttribute( indexArray, 1, false ); 63 | 64 | } 65 | 66 | const mdlpr = new Promise( ( resolve, reject ) => { 67 | 68 | new MDLLoader( this.manager ).load( `${ url }.mdl`, resolve, undefined, reject ); 69 | 70 | } ); 71 | 72 | const vvdpr = new Promise( ( resolve, reject ) => { 73 | 74 | new VVDLoader( this.manager ).load( `${ url }.vvd`, resolve, undefined, reject ); 75 | 76 | } ); 77 | 78 | const vtxpr = new Promise( ( resolve, reject ) => { 79 | 80 | new VTXLoader( this.manager ).load( `${ url }.dx90.vtx`, resolve, undefined, reject ); 81 | 82 | } ); 83 | 84 | Promise 85 | .all( [ mdlpr, vvdpr, vtxpr ] ) 86 | .then( ( [ mdl, vvd, vtx ] ) => { 87 | 88 | const promises = []; 89 | const vmtLoader = new VMTLoader( this.manager ); 90 | const tokens = url.split( '/models/' ); 91 | tokens.pop(); 92 | const path = tokens.join( '/models/' ) + '/materials/'; 93 | mdl.textures.forEach( t => { 94 | 95 | const matPromises = []; 96 | mdl.textureDirectories.forEach( f => { 97 | 98 | const vmtUrl = `${ path }${ f }${ t }.vmt`; 99 | const pr = new Promise( resolve => { 100 | 101 | vmtLoader.load( vmtUrl, material => { 102 | 103 | material.name = t; 104 | resolve( material ); 105 | 106 | }, undefined, () => resolve( null ) ); 107 | 108 | } ); 109 | matPromises.push( pr ); 110 | 111 | } ); 112 | 113 | promises.push( Promise.all( matPromises ).then( materials => materials.filter( m => ! ! m )[ 0 ] || new MeshPhongMaterial() ) ); 114 | 115 | } ); 116 | 117 | // TODO: Order is important here so it would be best to guarantee the order 118 | // in which the materials are specified 119 | return Promise 120 | .all( promises ) 121 | .then( materials => ( { materials, mdl, vvd, vtx } ) ); 122 | 123 | } ) 124 | .then( ( { mdl, vvd, vtx, materials } ) => { 125 | 126 | if ( mdl.header.checksum !== vvd.header.checksum || mdl.header.checksum !== vtx.header.checksum ) { 127 | 128 | console.warn( 'SourceModelLoader: File checksums do not match.' ); 129 | 130 | } 131 | 132 | // https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/utils/vrad/vradstaticprops.cpp#L1504-L1688 133 | if ( mdl.numbodyparts !== vtx.numBodyParts ) { 134 | 135 | console.warn( 'SourceModelLoader: Number of body parts does not match.' ); 136 | 137 | } 138 | 139 | materials.map( m => m.skinning = true ); 140 | const group = new Group(); 141 | const bones = mdl.bones.map( b => { 142 | 143 | const bone = new Bone(); 144 | bone.position.set( b.pos.x, b.pos.y, b.pos.z ); 145 | bone.quaternion.set( b.quaternion.x, b.quaternion.y, b.quaternion.z, b.quaternion.w ); 146 | return bone; 147 | 148 | } ); 149 | 150 | bones.forEach( ( b, i ) => { 151 | 152 | const parent = mdl.bones[ i ].parent; 153 | if ( parent === - 1 ) { 154 | 155 | group.add( b ); 156 | 157 | } else { 158 | 159 | bones[ parent ].add( b ); 160 | 161 | } 162 | 163 | } ); 164 | 165 | // create the shared skeleton and update all the bone matrices that have been added 166 | // into the group to ensure the inverses generated for the skeleton on bind are correct 167 | const skeleton = new Skeleton( bones ); 168 | group.updateMatrixWorld( true ); 169 | 170 | // TODO: make groups for body parts and models and apply names 171 | vtx.bodyParts.forEach( ( vtxBodyPart, i ) => { 172 | 173 | const mdlBodyPart = mdl.bodyParts[ i ]; 174 | if ( mdlBodyPart.nummodels !== vtxBodyPart.numModels ) { 175 | 176 | console.warn( 'SourceModelLoader: Number of models does not match.' ); 177 | 178 | } 179 | 180 | vtxBodyPart.models.forEach( ( vtxModel, i2 ) => { 181 | 182 | const mdlModel = mdlBodyPart.models[ i2 ]; 183 | vtxModel.lods.forEach( ( vtxLod, i3 ) => { 184 | 185 | // TODO: Skipping everything other than the highest 186 | // quality level of detail 187 | if ( i3 !== 0 ) return; 188 | 189 | if ( mdlModel.nummeshes !== vtxLod.numMeshes ) { 190 | 191 | console.warn( 'SourceModelLoader: Number of meshes does not match.', mdlModel.nummeshes, vtxLod.numMeshes ); 192 | return; 193 | 194 | } 195 | 196 | vtxLod.meshes.forEach( ( vtxMesh, i4 ) => { 197 | 198 | // TODO: Enable the ability to pick which skin family we use 199 | const mdlMesh = mdlModel.meshes[ i4 ]; 200 | const skinsTable = mdl.skinsTable; 201 | const material = materials[ skinsTable[ 0 ][ mdlMesh.material ] ]; 202 | 203 | vtxMesh.stripGroups.forEach( vtxStripGroup => { 204 | 205 | const obj = new Group(); 206 | 207 | vtxStripGroup.strips.forEach( vtxStrip => { 208 | 209 | // if ( s.indexOffset !== 0 || s.numIndices === 0 ) return; 210 | // console.log( vtxStrip.flags, vtxStrip ); 211 | 212 | const indexAttr = toGeometryIndex( vtx.buffer, mdlModel, mdlMesh, vtxStripGroup, vtxStrip ); 213 | const geometry = new BufferGeometry(); 214 | geometry.setIndex( indexAttr ); 215 | geometry.setAttribute( 'position', vvd.attributes.position ); 216 | geometry.setAttribute( 'uv', vvd.attributes.uv ); 217 | geometry.setAttribute( 'normal', vvd.attributes.normal ); 218 | geometry.setAttribute( 'skinWeight', vvd.attributes.skinWeight ); 219 | geometry.setAttribute( 'skinIndex', vvd.attributes.skinIndex ); 220 | 221 | // TODO : Winding order seems incorrect causing normals to face the wrong direction 222 | // the and faces to be inverted 223 | 224 | geometry.addGroup( vtxStrip.numIndices, vtxStrip.indexOffset, 0 ); 225 | 226 | const mesh = new SkinnedMesh( geometry, material ); 227 | mesh.bind( skeleton ); 228 | 229 | if ( vtxStrip.flags & 2 ) { 230 | 231 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); 232 | 233 | } 234 | 235 | obj.add( mesh ); 236 | mesh.userData.materialIndex = mdlMesh.material; 237 | 238 | } ); 239 | 240 | if ( obj.children.length === 1 ) { 241 | 242 | group.add( obj.children[ 0 ] ); 243 | 244 | } else { 245 | 246 | group.add( obj ); 247 | 248 | } 249 | 250 | } ); 251 | 252 | } ); 253 | 254 | } ); 255 | 256 | } ); 257 | 258 | } ); 259 | 260 | group.userData.skinsTable = mdl.skinsTable; 261 | group.userData.materials = materials; 262 | onLoad( { group, vvd, mdl, vtx, materials } ); 263 | 264 | } ); 265 | 266 | } 267 | 268 | } 269 | 270 | export { SourceModelLoader }; 271 | -------------------------------------------------------------------------------- /src/VMTLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultLoadingManager, 3 | FileLoader, 4 | MeshPhongMaterial, 5 | RepeatWrapping, 6 | LinearEncoding, 7 | } from 'three'; 8 | import { VTFLoader } from './VTFLoader.js'; 9 | 10 | // VMT: https://developer.valvesoftware.com/wiki/VMT 11 | 12 | function addExt( path, ext ) { 13 | 14 | const re = new RegExp( `.${ ext }$`, 'i' ); 15 | if ( re.test( path ) ) return path; 16 | else return `${ path }.${ ext }`; 17 | 18 | } 19 | 20 | const VMTLoader = function ( manager ) { 21 | 22 | this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; 23 | 24 | }; 25 | 26 | VMTLoader.prototype = { 27 | 28 | constructor: VMTLoader, 29 | 30 | load: function ( url, onLoad, onProgress, onError ) { 31 | 32 | const scope = this; 33 | 34 | const loader = new FileLoader( this.manager ); 35 | loader.setPath( this.path ); 36 | loader.setResponseType( 'text' ); 37 | loader.load( url, function ( text ) { 38 | 39 | onLoad( scope.parse( text, url ) ); 40 | 41 | }, onProgress, onError ); 42 | 43 | }, 44 | 45 | // TODO: Fix this url use and follow the "path" pattern of other loaders 46 | parse: function ( string, url ) { 47 | 48 | let type = ''; 49 | let root = null; 50 | const objects = []; 51 | let currData = ''; 52 | for ( let i = 0, l = string.length; i < l; i ++ ) { 53 | 54 | const c = string[ i ]; 55 | if ( c === '{' ) { 56 | 57 | const newObj = {}; 58 | if ( objects.length === 0 ) { 59 | 60 | type += currData; 61 | 62 | } else { 63 | 64 | objects[ objects.length - 1 ][ currData.trim() ] = newObj; 65 | 66 | } 67 | 68 | objects.push( newObj ); 69 | if ( root === null ) { 70 | 71 | root = newObj; 72 | 73 | } 74 | 75 | currData = ''; 76 | 77 | } else if ( c === '}' ) { 78 | 79 | objects.pop(); 80 | 81 | } else if ( c === '\n' ) { 82 | 83 | if ( objects.length === 0 ) { 84 | 85 | type += currData; 86 | 87 | } else { 88 | 89 | const tokens = currData.trim().split( /\s+/ ); 90 | if ( tokens.length >= 2 ) { 91 | 92 | const mapped = tokens.map( t => t.replace( /"/g, '' ) ); 93 | const name = mapped[ 0 ]; 94 | let contents = mapped[ 1 ]; 95 | 96 | if ( /^\[/.test( contents ) ) { 97 | 98 | contents = contents 99 | .replace( /[\[\]]/g, '' ) 100 | .split( /\s+/g ) 101 | .map( n => parseFloat( n ) ); 102 | 103 | } else if ( /^\d*\.?\d*$/.test( contents ) ) { 104 | 105 | contents = parseFloat( contents ); 106 | 107 | } 108 | 109 | objects[ objects.length - 1 ][ name ] = contents; 110 | 111 | 112 | } 113 | 114 | } 115 | 116 | currData = ''; 117 | 118 | } else { 119 | 120 | currData += c; 121 | 122 | } 123 | 124 | } 125 | 126 | // TODO: Repeat wrapping should be handled in the VFT loads 127 | const urlTokens = url.split( /materials/i ); 128 | urlTokens.pop(); 129 | 130 | const path = `${ urlTokens.join( 'materials' ) }materials/`; 131 | const material = new MeshPhongMaterial(); 132 | const vtfLoader = new VTFLoader( this.manager ); 133 | for ( const key in root ) { 134 | 135 | // TODO: Use more keys 136 | // TODO: bump map is causing all normals to disappear here 137 | const field = root[ key ]; 138 | switch ( key.toLowerCase() ) { 139 | 140 | case '$basetexture': 141 | material.map = vtfLoader.load( addExt( `${ path }${ field }`, 'vtf' ) ); 142 | material.map.wrapS = RepeatWrapping; 143 | material.map.wrapT = RepeatWrapping; 144 | material.map.encoding = LinearEncoding; 145 | break; 146 | case '$bumpmap': 147 | // TODO: This doesn't seem to quite map correctly to normal map 148 | // material.normalMap = vtfLoader.load( addExt( `${ path }${ field }`, '.vtf' ) ); 149 | // material.normalMap.wrapS = RepeatWrapping; 150 | // material.normalMap.wrapT = RepeatWrapping; 151 | break; 152 | case '$phongexponenttexture': 153 | // TODO: This doesn't quite map appropriately to a specular map 154 | material.specularMap = vtfLoader.load( addExt( `${ path }${ field }`, 'vtf' ) ); 155 | material.specularMap.wrapS = RepeatWrapping; 156 | material.specularMap.wrapT = RepeatWrapping; 157 | break; 158 | 159 | } 160 | 161 | } 162 | 163 | return material; 164 | 165 | } 166 | 167 | }; 168 | 169 | export { VMTLoader }; 170 | -------------------------------------------------------------------------------- /src/VTFLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | CompressedTextureLoader, 3 | RGBAFormat, 4 | RGBFormat, 5 | RGB_S3TC_DXT1_Format, 6 | RGBA_S3TC_DXT3_Format, 7 | RGBA_S3TC_DXT5_Format, 8 | LinearFilter, 9 | LinearMipmapLinearFilter, 10 | } from 'three'; 11 | 12 | // VTF: https://developer.valvesoftware.com/wiki/Valve_Texture_Format 13 | 14 | // TODO: The mipmap filter type needs to be updated to LinearFilter for some reason 15 | // TODO: get cube maps, animations, volume textures 16 | class VTFLoader extends CompressedTextureLoader { 17 | 18 | constructor( manager ) { 19 | 20 | super( manager ); 21 | this._parser = VTFLoader.parse; 22 | 23 | } 24 | 25 | parse( buffer ) { 26 | 27 | function bgrToRgb( buffer, stride ) { 28 | 29 | for ( let i = 0, l = buffer.length; i < l; i += stride ) { 30 | 31 | const b = buffer[ i ]; 32 | const r = buffer[ i + 2 ]; 33 | buffer[ i ] = r; 34 | buffer[ i + 2 ] = b; 35 | 36 | } 37 | 38 | } 39 | 40 | function parseHeader( buffer ) { 41 | 42 | const dataView = new DataView( buffer ); 43 | let i = 0; 44 | let signature = ''; 45 | for ( let j = 0; j < 4; j ++ ) { 46 | 47 | signature += String.fromCharCode( dataView.getUint8( i, true ) ); 48 | i ++; 49 | 50 | } 51 | 52 | const version = [ dataView.getUint32( i, true ), dataView.getUint32( i + 4, true ) ]; 53 | i += 8; 54 | 55 | const headerSize = dataView.getUint32( i, true ); 56 | i += 4; 57 | 58 | const width = dataView.getUint16( i, true ); 59 | i += 2; 60 | 61 | const height = dataView.getUint16( i, true ); 62 | i += 2; 63 | 64 | const flags = dataView.getUint32( i, true ); 65 | i += 4; 66 | 67 | const frames = dataView.getUint16( i, true ); 68 | i += 2; 69 | 70 | const firstFrame = dataView.getUint16( i, true ); 71 | i += 2; 72 | 73 | // padding0 74 | i += 4; 75 | 76 | const reflectivity = []; 77 | for ( let j = 0; j < 3; j ++ ) { 78 | 79 | reflectivity.push( dataView.getFloat32( i, true ) ); 80 | i += 4; 81 | 82 | } 83 | 84 | // padding1 85 | i += 4; 86 | 87 | const bumpmapScale = dataView.getFloat32( i, true ); 88 | i += 4; 89 | 90 | const highResImageFormat = dataView.getUint32( i, true ); 91 | i += 4; 92 | 93 | const mipmapCount = dataView.getUint8( i, true ); 94 | i += 1; 95 | 96 | const lowResImageFormat = dataView.getUint32( i, true ); 97 | i += 4; 98 | 99 | const lowResImageWidth = dataView.getUint8( i, true ); 100 | i += 1; 101 | 102 | const lowResImageHeight = dataView.getUint8( i, true ); 103 | i += 1; 104 | 105 | // 7.2+ 106 | const depth = dataView.getUint16( i, true ); 107 | i += 2; 108 | 109 | // 7.3+ 110 | // padding2 111 | i += 3; 112 | 113 | const numResources = dataView.getUint32( i, true ); 114 | i += 4; 115 | 116 | return { 117 | signature, 118 | version, 119 | headerSize, 120 | width, 121 | height, 122 | flags, 123 | frames, 124 | firstFrame, 125 | reflectivity, 126 | bumpmapScale, 127 | highResImageFormat, 128 | mipmapCount, 129 | lowResImageFormat, 130 | lowResImageWidth, 131 | lowResImageHeight, 132 | depth, 133 | numResources 134 | }; 135 | 136 | } 137 | 138 | function getMipMap( buffer, offset, format, width, height ) { 139 | 140 | const dxtSz = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4; 141 | let threeFormat = null; 142 | let byteArray = null; 143 | 144 | let dataLength; 145 | switch ( format ) { 146 | 147 | case 0: // RGBA8888 148 | dataLength = width * height * 4; 149 | byteArray = new Uint8Array( buffer, offset, dataLength ); 150 | threeFormat = RGBAFormat; 151 | break; 152 | case 1: // ABGR8888 153 | dataLength = width * height * 4; 154 | byteArray = new Uint8Array( buffer, offset, dataLength ); 155 | 156 | for ( let i = 0, l = byteArray.length; i < l; i += 4 ) { 157 | 158 | const a = byteArray[ i ]; 159 | const b = byteArray[ i + 1 ]; 160 | const g = byteArray[ i + 2 ]; 161 | const r = byteArray[ i + 3 ]; 162 | byteArray[ i ] = r; 163 | byteArray[ i + 1 ] = g; 164 | byteArray[ i + 2 ] = b; 165 | byteArray[ i + 3 ] = a; 166 | 167 | } 168 | 169 | threeFormat = RGBAFormat; 170 | break; 171 | case 2: // RGB888 172 | dataLength = width * height * 3; 173 | byteArray = new Uint8Array( buffer, offset, dataLength ); 174 | threeFormat = RGBFormat; 175 | break; 176 | case 3: // BGR888 177 | dataLength = width * height * 3; 178 | byteArray = new Uint8Array( buffer, offset, dataLength ); 179 | bgrToRgb( byteArray, 3 ); 180 | threeFormat = RGBFormat; 181 | break; 182 | case 12: // BGRA8888 183 | dataLength = width * height * 4; 184 | byteArray = new Uint8Array( buffer, offset, dataLength ); 185 | bgrToRgb( byteArray, 4 ); 186 | threeFormat = RGBAFormat; 187 | break; 188 | case 13: // DXT1 189 | dataLength = dxtSz * 8; // 8 blockBytes 190 | byteArray = new Uint8Array( buffer, offset, dataLength ); 191 | threeFormat = RGB_S3TC_DXT1_Format; 192 | break; 193 | case 14: // DXT3 194 | dataLength = dxtSz * 16; // 16 blockBytes 195 | byteArray = new Uint8Array( buffer, offset, dataLength ); 196 | threeFormat = RGBA_S3TC_DXT3_Format; 197 | break; 198 | case 15: // DXT5 199 | dataLength = dxtSz * 16; // 16 blockBytes 200 | byteArray = new Uint8Array( buffer, offset, dataLength ); 201 | threeFormat = RGBA_S3TC_DXT5_Format; 202 | break; 203 | default: 204 | console.error( `VTFLoader: Format letiant ${ format } is unsupported.` ); 205 | return null; 206 | 207 | } 208 | 209 | return { 210 | 211 | format: threeFormat, 212 | data: byteArray, 213 | width, 214 | height 215 | 216 | }; 217 | 218 | } 219 | 220 | function parseMipMaps( buffer, header ) { 221 | 222 | let offset = 80; 223 | if ( header.lowResImageHeight !== 0 ) { 224 | 225 | const lowResMap = getMipMap( buffer, offset, header.lowResImageFormat, header.lowResImageWidth, header.lowResImageHeight ); 226 | offset += lowResMap.data.length; 227 | 228 | if ( header.version[ 0 ] > 7 || header.version[ 1 ] >= 3 ) { 229 | 230 | offset += header.headerSize - 80; 231 | 232 | } 233 | 234 | } 235 | 236 | const dimensions = new Array( header.mipmapCount ); 237 | let currWidth = header.width; 238 | let currHeight = header.height; 239 | for ( let i = header.mipmapCount - 1; i >= 0; i -- ) { 240 | 241 | dimensions[ i ] = { width: currWidth, height: currHeight }; 242 | currWidth = ( currWidth >> 1 ) || 1; 243 | currHeight = ( currHeight >> 1 ) || 1; 244 | 245 | } 246 | 247 | // smallest to largest 248 | let mipmaps = []; 249 | for ( let i = 0; i < header.mipmapCount; i ++ ) { 250 | 251 | let { width, height } = dimensions[ i ]; 252 | const map = getMipMap( buffer, offset, header.highResImageFormat, width, height ); 253 | mipmaps.push( map ); 254 | offset += map.data.length; 255 | 256 | width = width << 1; 257 | height = height << 1; 258 | 259 | } 260 | 261 | mipmaps = mipmaps.reverse(); 262 | 263 | return { 264 | 265 | mipmaps: mipmaps, 266 | width: header.width, 267 | height: header.height, 268 | format: mipmaps[ 0 ].format, 269 | mipmapCount: mipmaps.length 270 | 271 | }; 272 | 273 | } 274 | 275 | const header = parseHeader( buffer ); 276 | return parseMipMaps( buffer, header ); 277 | 278 | } 279 | 280 | load( url, onComplete, ...rest ) { 281 | 282 | const tex = CompressedTextureLoader.prototype.load.call( 283 | this, 284 | url, 285 | tex => { 286 | 287 | // set unpack alignment to 1 if using 3-stride RGB data 288 | if ( tex.format === RGBFormat ) { 289 | 290 | tex.unpackAlignment = 1; 291 | 292 | } 293 | 294 | if ( onComplete ) { 295 | 296 | onComplete( tex ); 297 | 298 | } 299 | 300 | }, 301 | ...rest 302 | ); 303 | 304 | 305 | tex.minFilter = LinearMipmapLinearFilter; 306 | tex.magFilter = LinearFilter; 307 | 308 | return tex; 309 | 310 | } 311 | 312 | } 313 | 314 | export { VTFLoader }; 315 | -------------------------------------------------------------------------------- /src/VTXLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultLoadingManager, 3 | FileLoader 4 | } from 'three'; 5 | 6 | // VTX: https://developer.valvesoftware.com/wiki/VTX 7 | 8 | const VTXLoader = function ( manager ) { 9 | 10 | this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; 11 | 12 | }; 13 | 14 | VTXLoader.prototype = { 15 | 16 | constructor: VTXLoader, 17 | 18 | load: function ( url, onLoad, onProgress, onError ) { 19 | 20 | const scope = this; 21 | 22 | const loader = new FileLoader( this.manager ); 23 | loader.setPath( this.path ); 24 | loader.setResponseType( 'arraybuffer' ); 25 | loader.load( url, function ( text ) { 26 | 27 | onLoad( scope.parse( text ) ); 28 | 29 | }, onProgress, onError ); 30 | 31 | }, 32 | 33 | parse: function ( buffer ) { 34 | 35 | function readString( dataView, offset, count = Infinity ) { 36 | 37 | let str = ''; 38 | for ( let j = 0; j < count; j ++ ) { 39 | 40 | const c = dataView.getUint8( j + offset ); 41 | if ( c === 0 ) break; 42 | 43 | str += String.fromCharCode( c ); 44 | 45 | } 46 | 47 | return str; 48 | 49 | } 50 | 51 | // struct FileHeader_t 52 | function parseHeader( buffer ) { 53 | 54 | const dataView = new DataView( buffer ); 55 | let i = 0; 56 | 57 | // int 58 | const version = dataView.getInt32( i, true ); 59 | i += 4; 60 | 61 | // int 62 | const vertCacheSize = dataView.getInt32( i, true ); 63 | i += 4; 64 | 65 | // short 66 | const maxBonesPerStrip = dataView.getUint16( i, true ); 67 | i += 2; 68 | 69 | // short 70 | const maxBonesPerTri = dataView.getUint16( i, true ); 71 | i += 2; 72 | 73 | // int 74 | const maxBonesPerVert = dataView.getInt32( i, true ); 75 | i += 4; 76 | 77 | // int 78 | const checksum = dataView.getInt32( i, true ); 79 | i += 4; 80 | 81 | // int 82 | const numLODs = dataView.getInt32( i, true ); 83 | i += 4; 84 | 85 | // int 86 | const materialReplacementListOffset = dataView.getInt32( i, true ); 87 | i += 4; 88 | 89 | // int 90 | const numBodyParts = dataView.getInt32( i, true ); 91 | i += 4; 92 | 93 | // int 94 | const bodyPartOffset = dataView.getInt32( i, true ); 95 | i += 4; 96 | 97 | return { 98 | version, 99 | vertCacheSize, 100 | maxBonesPerStrip, 101 | maxBonesPerTri, 102 | maxBonesPerVert, 103 | checksum, 104 | numLODs, 105 | materialReplacementListOffset, 106 | numBodyParts, 107 | bodyPartOffset 108 | }; 109 | 110 | 111 | } 112 | 113 | // struct StripHeader_t 114 | function parseStrips( buffer, numStrips, stripOffset ) { 115 | 116 | const dataView = new DataView( buffer ); 117 | const res = []; 118 | 119 | for ( let i = 0; i < numStrips; i ++ ) { 120 | 121 | // TODO: This offset seems to make things work correctly for the ball and chain 122 | // but it's unclear why... padding? 123 | // var offset = stripOffset + i * 27; 124 | const offset = stripOffset + i * 35; 125 | const strip = {}; 126 | strip.numIndices = dataView.getInt32( offset + 0, true ); 127 | strip.indexOffset = dataView.getInt32( offset + 4, true ); 128 | 129 | strip.numVerts = dataView.getInt32( offset + 8, true ); 130 | strip.vertOffset = dataView.getInt32( offset + 12, true ); 131 | 132 | strip.numBones = dataView.getInt16( offset + 16, true ); 133 | 134 | strip.flags = dataView.getUint8( offset + 18, true ); 135 | 136 | // TODO: parse these into an array 137 | strip.numBoneStateChanges = dataView.getInt32( offset + 19, true ); 138 | strip.boneStateChangeOffset = dataView.getInt32( offset + 23, true ); 139 | 140 | 141 | res.push( strip ); 142 | 143 | } 144 | 145 | return res; 146 | 147 | } 148 | 149 | // struct StripGroupHeader_t 150 | function parseStripGroups( buffer, numStripGroups, stripGroupHeaderOffset ) { 151 | 152 | const dataView = new DataView( buffer ); 153 | const res = []; 154 | for ( let i = 0; i < numStripGroups; i ++ ) { 155 | 156 | // TODO: Looking at the padding offsets in the MGSBox model it looks like 157 | // this struct has as stride of 33 but counting up yields 25? 158 | // var offset = stripGroupHeaderOffset + i * 25; 159 | const offset = stripGroupHeaderOffset + i * 33; 160 | const stripGroup = {}; 161 | stripGroup.numVerts = dataView.getInt32( offset + 0, true ); 162 | stripGroup.vertOffset = dataView.getInt32( offset + 4, true ); 163 | 164 | stripGroup.numIndices = dataView.getInt32( offset + 8, true ); 165 | stripGroup.indexOffset = dataView.getInt32( offset + 12, true ); 166 | 167 | stripGroup.numStrips = dataView.getInt32( offset + 16, true ); 168 | stripGroup.stripOffset = dataView.getInt32( offset + 20, true ); 169 | 170 | stripGroup.flags = dataView.getUint8( offset + 24, true ); 171 | 172 | stripGroup.strips = parseStrips( buffer, stripGroup.numStrips, offset + stripGroup.stripOffset ); 173 | 174 | stripGroup.indexDataStart = offset + stripGroup.indexOffset; 175 | stripGroup.vertexDataStart = offset + stripGroup.vertOffset; 176 | 177 | res.push( stripGroup ); 178 | 179 | } 180 | 181 | return res; 182 | 183 | } 184 | 185 | // struct MeshHeader_t 186 | function parseMeshes( buffer, numMeshes, meshOffset ) { 187 | 188 | const dataView = new DataView( buffer ); 189 | const res = []; 190 | for ( let i = 0; i < numMeshes; i ++ ) { 191 | 192 | const offset = meshOffset + i * 9; 193 | const mesh = {}; 194 | mesh.numStripGroups = dataView.getInt32( offset + 0, true ); 195 | mesh.stripGroupHeaderOffset = dataView.getInt32( offset + 4, true ); 196 | mesh.flags = dataView.getUint8( offset + 8, true ); 197 | mesh.stripGroups = parseStripGroups( buffer, mesh.numStripGroups, offset + mesh.stripGroupHeaderOffset ); 198 | res.push( mesh ); 199 | 200 | } 201 | 202 | return res; 203 | 204 | } 205 | 206 | // struct ModelLODHeader_t 207 | function parseLods( buffer, numLODs, lodOffset ) { 208 | 209 | const dataView = new DataView( buffer ); 210 | const res = []; 211 | for ( let i = 0; i < numLODs; i ++ ) { 212 | 213 | const offset = lodOffset + i * 12; 214 | const lod = {}; 215 | lod.numMeshes = dataView.getInt32( offset + 0, true ); 216 | lod.meshOffset = dataView.getInt32( offset + 4, true ); 217 | lod.switchPoint = dataView.getFloat32( offset + 8, true ); 218 | lod.meshes = parseMeshes( buffer, lod.numMeshes, offset + lod.meshOffset ); 219 | 220 | res.push( lod ); 221 | 222 | } 223 | 224 | return res; 225 | 226 | } 227 | 228 | // struct ModelHeader_t 229 | function parseModels( buffer, numModels, modelOffset ) { 230 | 231 | const dataView = new DataView( buffer ); 232 | const res = []; 233 | for ( let i = 0; i < numModels; i ++ ) { 234 | 235 | const offset = modelOffset + i * 8; 236 | const model = {}; 237 | model.numLODs = dataView.getInt32( offset + 0, true ); 238 | model.lodOffset = dataView.getInt32( offset + 4, true ); 239 | model.lods = parseLods( buffer, model.numLODs, offset + model.lodOffset ); 240 | 241 | res.push( model ); 242 | 243 | } 244 | 245 | return res; 246 | 247 | } 248 | 249 | // struct BodyPartHeader_t 250 | function parseBodyParts( buffer, numBodyParts, bodyPartOffset ) { 251 | 252 | const dataView = new DataView( buffer ); 253 | const res = []; 254 | for ( let i = 0; i < numBodyParts; i ++ ) { 255 | 256 | const offset = bodyPartOffset + i * 8; 257 | const bodyPart = {}; 258 | bodyPart.numModels = dataView.getInt32( offset + 0, true ); 259 | bodyPart.modelOffset = dataView.getInt32( offset + 4, true ); 260 | bodyPart.models = parseModels( buffer, bodyPart.numModels, offset + bodyPart.modelOffset ); 261 | 262 | res.push( bodyPart ); 263 | 264 | } 265 | 266 | return res; 267 | 268 | } 269 | 270 | function parseMaterialReplacement( buffer, matReplacementNum, matReplacementOffset ) { 271 | 272 | const dataView = new DataView( buffer ); 273 | const res = []; 274 | for ( let i = 0; i < matReplacementNum; i ++ ) { 275 | 276 | const offset = matReplacementOffset + i * 8; 277 | const replaceMaterial = {}; 278 | replaceMaterial.numReplacements = dataView.getInt32( offset + 0, true ); 279 | replaceMaterial.replacementOffset = dataView.getInt32( offset + 4, true ); 280 | replaceMaterial.replacements = []; 281 | 282 | for ( let j = 0; j < replaceMaterial.numReplacements; j ++ ) { 283 | 284 | const offset2 = replaceMaterial.replacementOffset + i * 6; 285 | const replacement = {}; 286 | replacement.materialID = dataView.getInt16( offset2 + 0, true ); 287 | replacement.name = readString( dataView, dataView.getInt32( offset2 + 2, true ) ); 288 | 289 | } 290 | 291 | res.push( replaceMaterial ); 292 | 293 | } 294 | 295 | return res; 296 | 297 | } 298 | 299 | const header = parseHeader( buffer ); 300 | const bodyParts = parseBodyParts( buffer, header.numBodyParts, header.bodyPartOffset ); 301 | const materialReplacements = parseMaterialReplacement( buffer, header.numLODs, header.materialReplacementListOffset ); 302 | 303 | return { header, bodyParts, materialReplacements, buffer }; 304 | 305 | } 306 | 307 | }; 308 | 309 | export { VTXLoader }; 310 | -------------------------------------------------------------------------------- /src/VVDLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultLoadingManager, 3 | FileLoader, 4 | InterleavedBuffer, 5 | BufferAttribute, 6 | InterleavedBufferAttribute 7 | } from 'three'; 8 | 9 | 10 | // VVD: https://developer.valvesoftware.com/wiki/VVD 11 | 12 | function memcopy( dst, dstStart, src, srcStart, len ) { 13 | 14 | for ( let i = 0; i < len; i ++ ) { 15 | 16 | dst[ dstStart + i ] = src[ srcStart + i ]; 17 | 18 | } 19 | 20 | } 21 | 22 | const VVDLoader = function ( manager ) { 23 | 24 | this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; 25 | 26 | }; 27 | 28 | VVDLoader.prototype = { 29 | 30 | constructor: VVDLoader, 31 | 32 | load: function ( url, onLoad, onProgress, onError ) { 33 | 34 | const scope = this; 35 | 36 | const loader = new FileLoader( this.manager ); 37 | loader.setPath( this.path ); 38 | loader.setResponseType( 'arraybuffer' ); 39 | loader.load( url, function ( text ) { 40 | 41 | onLoad( scope.parse( text ) ); 42 | 43 | }, onProgress, onError ); 44 | 45 | }, 46 | 47 | parse: function ( buffer ) { 48 | 49 | // https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/sp/src/public/studio.h#L398 50 | const MAX_NUM_LODS = 8; 51 | // const MAX_NUM_BONES_PER_VERT = 3; 52 | 53 | // struct vertexFileHeader_t 54 | function parseHeader( buffer ) { 55 | 56 | const dataView = new DataView( buffer ); 57 | let i = 0; 58 | 59 | // int 60 | const id = dataView.getInt32( i, true ); 61 | i += 4; 62 | 63 | // int 64 | const version = dataView.getInt32( i, true ); 65 | i += 4; 66 | 67 | // long 68 | const checksum = dataView.getInt32( i, true ); 69 | i += 4; 70 | 71 | // int 72 | const numLODs = dataView.getUint32( i, true ); 73 | i += 4; 74 | 75 | // int 76 | const numLODVertexes = []; 77 | for ( let j = 0; j < MAX_NUM_LODS; j ++ ) { 78 | 79 | numLODVertexes.push( dataView.getInt32( i, true ) ); 80 | i += 4; 81 | 82 | } 83 | 84 | // int 85 | const numFixups = dataView.getInt32( i, true ); 86 | i += 4; 87 | 88 | // int 89 | const fixupTableStart = dataView.getInt32( i, true ); 90 | i += 4; 91 | 92 | // int 93 | const vertexDataStart = dataView.getInt32( i, true ); 94 | i += 4; 95 | 96 | // int 97 | const tangentDataStart = dataView.getInt32( i, true ); 98 | i += 4; 99 | 100 | return { 101 | id, 102 | version, 103 | checksum, 104 | numLODs, 105 | numLODVertexes, 106 | numFixups, 107 | fixupTableStart, 108 | vertexDataStart, 109 | tangentDataStart, 110 | buffer 111 | }; 112 | 113 | } 114 | 115 | function parseFixups( buffer, numFixups, fixupTableStart ) { 116 | 117 | const dataView = new DataView( buffer ); 118 | let offset = fixupTableStart; 119 | const res = []; 120 | for ( let i = 0; i < numFixups; i ++ ) { 121 | 122 | const fixup = {}; 123 | fixup.lod = dataView.getInt32( offset + 0, true ); 124 | fixup.sourceVertexID = dataView.getInt32( offset + 4, true ); 125 | fixup.numVertexes = dataView.getInt32( offset + 8, true ); 126 | offset += 12; 127 | 128 | res.push( fixup ); 129 | 130 | } 131 | 132 | return res; 133 | 134 | } 135 | 136 | function getBufferAttribute( buffer, len, start ) { 137 | 138 | const interleavedFloat32Array = new Float32Array( buffer, start, len / 4 ); 139 | const interleavedFloat32Buffer = new InterleavedBuffer( interleavedFloat32Array, 48 / 4 ); 140 | const interleavedUint8Array = new Uint8Array( buffer, start, len ); 141 | const interleavedUint8Buffer = new InterleavedBuffer( interleavedUint8Array, 48 ); 142 | 143 | // VVD file describes three bone weights and indices while THREE.js requires four 144 | const totalVerts = len / 48; 145 | const skinWeightArray = new Float32Array( totalVerts * 4 ); 146 | const skinIndexArray = new Uint8Array( totalVerts * 4 ); 147 | 148 | for ( let i = 0; i < totalVerts; i ++ ) { 149 | 150 | const i4 = i * 4; 151 | const floatIndex = i * 12; 152 | skinWeightArray[ i4 + 0 ] = interleavedFloat32Array[ floatIndex + 0 ]; 153 | skinWeightArray[ i4 + 1 ] = interleavedFloat32Array[ floatIndex + 1 ]; 154 | skinWeightArray[ i4 + 2 ] = interleavedFloat32Array[ floatIndex + 2 ]; 155 | skinWeightArray[ i4 + 3 ] = 0; 156 | 157 | const uint8Index = i * 12 * 4 + 12; 158 | skinIndexArray[ i4 + 0 ] = interleavedUint8Array[ uint8Index + 0 ]; 159 | skinIndexArray[ i4 + 1 ] = interleavedUint8Array[ uint8Index + 1 ]; 160 | skinIndexArray[ i4 + 2 ] = interleavedUint8Array[ uint8Index + 2 ]; 161 | skinIndexArray[ i4 + 3 ] = 0; 162 | 163 | } 164 | 165 | return { 166 | 167 | skinWeight: new BufferAttribute( skinWeightArray, 4, false ), 168 | skinIndex: new BufferAttribute( skinIndexArray, 4, false ), 169 | numBones: new InterleavedBufferAttribute( interleavedUint8Buffer, 1, 15, false ), 170 | 171 | position: new InterleavedBufferAttribute( interleavedFloat32Buffer, 3, 4, false ), 172 | normal: new InterleavedBufferAttribute( interleavedFloat32Buffer, 3, 7, false ), 173 | uv: new InterleavedBufferAttribute( interleavedFloat32Buffer, 2, 10, false ), 174 | 175 | }; 176 | 177 | } 178 | 179 | const header = parseHeader( buffer ); 180 | const fixups = parseFixups( buffer, header.numFixups, header.fixupTableStart ); 181 | 182 | // apply fixups 183 | let attributes; 184 | const vertArrayLength = header.tangentDataStart - header.vertexDataStart; 185 | if ( fixups.length !== 0 ) { 186 | 187 | const vertexDataStart = header.vertexDataStart; 188 | const newBuffer = new ArrayBuffer( vertArrayLength ); 189 | const newUint8Buffer = new Uint8Array( newBuffer ); 190 | const ogUint8Buffer = new Uint8Array( buffer ); 191 | let target = 0; 192 | for ( let i = 0; i < fixups.length; i ++ ) { 193 | 194 | const fixup = fixups[ i ]; 195 | memcopy( 196 | newUint8Buffer, 197 | target * 48, 198 | ogUint8Buffer, 199 | vertexDataStart + fixup.sourceVertexID * 48, 200 | fixup.numVertexes * 48, 201 | ); 202 | target += fixup.numVertexes; 203 | 204 | } 205 | 206 | attributes = getBufferAttribute( newBuffer, vertArrayLength, 0 ); 207 | 208 | } else { 209 | 210 | attributes = getBufferAttribute( buffer, vertArrayLength, header.vertexDataStart ); 211 | 212 | } 213 | 214 | return { header, fixups, attributes, buffer }; 215 | 216 | } 217 | 218 | }; 219 | 220 | export { VVDLoader }; 221 | --------------------------------------------------------------------------------