├── .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 | [](https://github.com/gkjohnson/source-engine-model-loader/actions)
4 | [](https://github.com/gkjohnson/source-engine-model-loader/)
5 | [](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 | 
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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------