Last active
April 24, 2026 21:44
-
-
Save ariankordi/f28cf53a0e46e2a034949f44eeea0265 to your computer and use it in GitHub Desktop.
IQM shape parser in Fusion + Three.js viewer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Array-only attribute conversion utilities. | |
| public static class AttributeConverter | |
| { | |
| // NOTE: Only C++, JS, and #else implementations were tested. | |
| /// Copy numComponents little-endian f32 values per record from src into dst. | |
| public static void Float32LECopy(byte[] src, uint srcOff, float[]! dst, uint dstOff, | |
| uint numComponents, uint srcStride, uint recordCount) | |
| { | |
| #if C || CPP | |
| native { | |
| for (int ri = 0; ri < recordCount; ri++) { | |
| for (int ci = 0; ci < numComponents; ci++) { | |
| const unsigned char *b = (const unsigned char *)src + srcOff + ri * srcStride + ci * 4; | |
| unsigned bits = (unsigned)b[0] | ((unsigned)b[1] << 8) | ((unsigned)b[2] << 16) | ((unsigned)b[3] << 24); | |
| dst[dstOff + ri * numComponents + ci] = *(float *)&bits; | |
| } | |
| } | |
| } | |
| #elif JS || TS | |
| native { | |
| // NOTE: Assumes a little-endian JavaScript engine. | |
| const f32 = new Float32Array(src.buffer, | |
| src.byteOffset, recordCount * numComponents); | |
| for (let ri = 0; ri < recordCount; ri++) { | |
| const base = (srcOff + ri * srcStride) >>> 2; | |
| for (let ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = f32[base + ci]; | |
| } | |
| } | |
| #elif PY | |
| native { | |
| import struct as _struct | |
| for ri in range(recordCount): | |
| vals = _struct.unpack_from(f"<{numComponents}f", src, srcOff + ri * srcStride) | |
| for ci, v in enumerate(vals): | |
| dst[dstOff + ri * numComponents + ci] = v | |
| } | |
| #elif CS | |
| native { | |
| for (int ri = 0; ri < recordCount; ri++) | |
| for (int ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = | |
| System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian( | |
| new System.ReadOnlySpan<byte>(src, (int)(srcOff + ri * srcStride + ci * 4), 4)); | |
| } | |
| #elif JAVA | |
| native { | |
| java.nio.ByteBuffer buf = java.nio.ByteBuffer.wrap(src).order(java.nio.ByteOrder.LITTLE_ENDIAN); | |
| for (int ri = 0; ri < recordCount; ri++) | |
| for (int ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = buf.getFloat((int)(srcOff + ri * srcStride + ci * 4)); | |
| } | |
| #else | |
| for (uint ri = 0; ri < recordCount; ri++) | |
| for (uint ci = 0; ci < numComponents; ci++) | |
| { | |
| int bi = srcOff + ri * srcStride + ci * 4; | |
| uint bits = src[bi] | (src[bi + 1] << 8) | (src[bi + 2] << 16) | (src[bi + 3] << 24); | |
| dst[dstOff + ri * numComponents + ci] = BitConverter.Int32BitsToSingle(bits); | |
| // dst[dstOff + ri * numComponents + ci] = BitsToFloat(bits); | |
| } | |
| #endif | |
| } | |
| /// Copy numComponents big-endian f32 values per record from src into dst. | |
| public static void Float32BECopy(byte[] src, uint srcOff, float[]! dst, uint dstOff, | |
| uint numComponents, uint srcStride, uint recordCount) | |
| { | |
| #if C || CPP | |
| native { | |
| for (int ri = 0; ri < recordCount; ri++) { | |
| for (int ci = 0; ci < numComponents; ci++) { | |
| const unsigned char *b = (const unsigned char *)src + srcOff + ri * srcStride + ci * 4; | |
| unsigned bits = ((unsigned)b[0] << 24) | ((unsigned)b[1] << 16) | ((unsigned)b[2] << 8) | (unsigned)b[3]; | |
| dst[dstOff + ri * numComponents + ci] = *(float *)&bits; | |
| } | |
| } | |
| } | |
| #elif JS || TS | |
| native { | |
| const dv = new DataView(src.buffer, src.byteOffset); | |
| for (let ri = 0; ri < recordCount; ri++) | |
| for (let ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = dv.getFloat32(srcOff + ri * srcStride + ci * 4, false); | |
| } | |
| #elif PY | |
| native { | |
| import struct as _struct | |
| for ri in range(recordCount): | |
| vals = _struct.unpack_from(f">{numComponents}f", src, srcOff + ri * srcStride) | |
| for ci, v in enumerate(vals): | |
| dst[dstOff + ri * numComponents + ci] = v | |
| } | |
| #elif CS | |
| native { | |
| for (int ri = 0; ri < recordCount; ri++) | |
| for (int ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = | |
| System.Buffers.Binary.BinaryPrimitives.ReadSingleBigEndian( | |
| new System.ReadOnlySpan<byte>(src, (int)(srcOff + ri * srcStride + ci * 4), 4)); | |
| } | |
| #elif JAVA | |
| native { | |
| java.nio.ByteBuffer buf = java.nio.ByteBuffer.wrap(src).order(java.nio.ByteOrder.BIG_ENDIAN); | |
| for (int ri = 0; ri < recordCount; ri++) | |
| for (int ci = 0; ci < numComponents; ci++) | |
| dst[dstOff + ri * numComponents + ci] = buf.getFloat((int)(srcOff + ri * srcStride + ci * 4)); | |
| } | |
| #else | |
| for (uint ri = 0; ri < recordCount; ri++) | |
| for (uint ci = 0; ci < numComponents; ci++) | |
| { | |
| int bi = srcOff + ri * srcStride + ci * 4; | |
| uint bits = (src[bi] << 24) | (src[bi + 1] << 16) | (src[bi + 2] << 8) | src[bi + 3]; | |
| dst[dstOff + ri * numComponents + ci] = BitsToFloat(bits); | |
| } | |
| #endif | |
| } | |
| /// Reinterpret a 32-bit integer bit pattern as an IEEE 754 float. | |
| /// Optimized for 3D model data where denormalized/Infinity/NaN | |
| /// are uncommon and are just asserted against. | |
| static float BitsToFloat(int bits) | |
| { | |
| int exp = (bits >> 23) & 0xFF; | |
| int frac = bits & 0x7FFFFF; | |
| if (exp == 0) | |
| { | |
| assert frac == 0, "Unexpected denormalized float in vertex data"; | |
| return 0.0; | |
| } | |
| assert exp != 255, "Unexpected Infinity/NaN float in vertex data"; | |
| int sign = bits & 0x80000000; | |
| bool hasSign = sign != 0; | |
| float mantissa = 1.0 + frac / 8388608.0; // 2^23 | |
| float value = mantissa * Math.Pow(2.0, exp - 127); | |
| return hasSign ? -value : value; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>IQM Viewer</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| font-family: monospace; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| #info { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| color: white; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 10px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| max-width: 300px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="info">Loading IQM model...</div> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://esm.sh/three@r180", | |
| "three/addons/controls/OrbitControls.js": "https://esm.sh/three@r180/examples/jsm/controls/OrbitControls.js" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| // @ts-check | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { Ptr, IqmAccessor, IqmVertexArrayType, IqmTest } from './IqmTest.mjs'; | |
| // Set up the scene, with coordinate system set to +Z up. | |
| const scene = new THREE.Scene(); | |
| scene.rotation.x = -Math.PI / 2; | |
| scene.rotation.z = -Math.PI / 2; | |
| scene.background = new THREE.Color(0x222222); | |
| const camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000 | |
| ); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.body.appendChild(renderer.domElement); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| const infoDiv = /** @type {HTMLElement} */ (document.getElementById('info')); | |
| function displayModel(/** @type {Uint8Array} */ byteArray) { | |
| infoDiv.textContent = 'Parsing model...'; | |
| const ptr = new Ptr(); | |
| ptr.initialize(byteArray, 0); | |
| const accessor = new IqmAccessor(); | |
| accessor.initialize(ptr); | |
| const numVertexes = accessor.getNumVertexes(); | |
| const numTriangles = accessor.getNumTriangles(); | |
| // Positions | |
| const posIndex = accessor.findVertexArray(IqmVertexArrayType.POSITION); | |
| if (posIndex < 0) { | |
| throw new Error('Position attribute not found'); | |
| } | |
| const positions = new Float32Array(accessor.getAttributeFloatCount(posIndex)); | |
| accessor.normalizeAttribute(posIndex, positions, 0); | |
| // Normals (if available) | |
| let normals = null; | |
| const normIndex = accessor.findVertexArray(IqmVertexArrayType.NORMAL); | |
| if (normIndex >= 0) { | |
| normals = new Float32Array(accessor.getAttributeFloatCount(normIndex)); | |
| accessor.normalizeAttribute(normIndex, normals, 0); | |
| } | |
| // Triangle indices | |
| const indices = new Uint32Array(numTriangles * 3); | |
| accessor.getTriangleIndices(indices); | |
| infoDiv.textContent = `Loading: ${numVertexes} vertices, ${numTriangles} triangles...`; | |
| // Create geometry | |
| const geometry = new THREE.BufferGeometry(); | |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| if (normals) { | |
| geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3)); | |
| } | |
| geometry.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| // Bounding box | |
| const bounds = new Float32Array(6); | |
| accessor.getBounds(bounds); | |
| const box = new THREE.Box3(new THREE.Vector3().fromArray(bounds), new THREE.Vector3().fromArray(bounds, 3)); | |
| // geometry.computeBoundingBox(); | |
| // const box = /** @type {THREE.Box3} */ (geometry.boundingBox); | |
| // Create mesh | |
| const material = new THREE.MeshNormalMaterial({ | |
| // color: new THREE.Color('white'), | |
| wireframe: false, | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| scene.add(mesh); | |
| // Position camera based on bounds: https://github.com/donmccurdy/three-gltf-viewer/blob/bd83b39339a525708387006aba976342f036713e/src/viewer.js#L251-L276 | |
| // const box = new THREE.Box3().setFromObject(mesh); | |
| const size = box.getSize(new THREE.Vector3()).length(); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| mesh.position.x -= center.x; | |
| mesh.position.y -= center.y; | |
| mesh.position.z -= center.z; | |
| camera.position.copy(center); | |
| camera.position.x += size / 2.0; | |
| camera.position.y += size / 5.0; | |
| camera.position.z += size / 2.0; | |
| camera.lookAt(center); | |
| // Window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| infoDiv.textContent = `✓ Loaded ${numVertexes} vertices, ${numTriangles} triangles\nUse mouse to rotate, scroll to zoom`; | |
| // Render loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| } | |
| // infoDiv.textContent = 'Fetching model...'; | |
| // displayModel(new Uint8Array(await (await fetch('mrfixit.iqm')).arrayBuffer())); | |
| displayModel(IqmTest.getTestData()); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Struct layouts and field offsets are derived from iqm.h, part of the | |
| // Inter-Quake Model (IQM) format by Lee Salzman. | |
| // https://github.com/lsalzman/iqm | |
| /// Vertex attribute semantic type codes from iqmvertexarray.type. | |
| public enum IqmVertexArrayType | |
| { | |
| Position = 0, | |
| TexCoord = 1, | |
| Normal = 2, | |
| Tangent = 3, | |
| BlendIndexes = 4, | |
| BlendWeights = 5, | |
| Color = 6, | |
| Custom = 16 | |
| } | |
| /// Vertex attribute element format codes from iqmvertexarray.format. | |
| public enum IqmFormat | |
| { | |
| Byte = 0, | |
| UByte = 1, | |
| Short = 2, | |
| UShort = 3, | |
| Int = 4, | |
| UInt = 5, | |
| Half = 6, | |
| Float = 7, | |
| Double = 8 | |
| } | |
| /// Accessor for iqmheader (124 bytes, little-endian). | |
| public static class IqmHeader | |
| { | |
| public const uint Length = 124; | |
| public static uint GetVersion(Ptr p) => p.ReadU32LE(0x10); | |
| public static uint GetFileSize(Ptr p) => p.ReadU32LE(0x14); | |
| public static uint GetNumMeshes(Ptr p) => p.ReadU32LE(0x24); | |
| public static uint GetOfsMeshes(Ptr p) => p.ReadU32LE(0x28); | |
| public static uint GetNumVertexArrays(Ptr p) => p.ReadU32LE(0x2c); | |
| public static uint GetNumVertexes(Ptr p) => p.ReadU32LE(0x30); | |
| public static uint GetOfsVertexArrays(Ptr p) => p.ReadU32LE(0x34); | |
| public static uint GetNumTriangles(Ptr p) => p.ReadU32LE(0x38); | |
| public static uint GetOfsTriangles(Ptr p) => p.ReadU32LE(0x3c); | |
| public static uint GetOfsBounds(Ptr p) => p.ReadU32LE(0x68); | |
| } | |
| /// Accessor for iqmvertexarray (20 bytes, little-endian). | |
| /// size is the number of components per vertex (e.g. 3 for xyz position). | |
| /// offset is the byte offset of the attribute data within the file. | |
| public static class IqmVertexArray | |
| { | |
| public const uint Length = 20; | |
| /// Type discriminator: returned as int to allow enum comparison via FromInt. | |
| public static int GetType(Ptr p) => p.ReadI32LE(0); | |
| public static uint GetFlags(Ptr p) => p.ReadU32LE(4); | |
| /// Format discriminator: returned as int to allow enum comparison via FromInt. | |
| public static int GetFormat(Ptr p) => p.ReadI32LE(8); | |
| /// Component count per vertex (e.g. 3 for xyz). | |
| public static uint GetSize(Ptr p) => p.ReadU32LE(12); | |
| /// Byte offset of attribute data within the file. | |
| public static uint GetOffset(Ptr p) => p.ReadU32LE(16); | |
| } | |
| /// Accessor for iqmmesh (24 bytes, little-endian). | |
| public static class IqmMesh | |
| { | |
| public const uint Length = 24; | |
| public static uint GetName(Ptr p) => p.ReadU32LE(0); | |
| public static uint GetMaterial(Ptr p) => p.ReadU32LE(4); | |
| public static uint GetFirstVertex(Ptr p) => p.ReadU32LE(8); | |
| public static uint GetNumVertexes(Ptr p) => p.ReadU32LE(12); | |
| public static uint GetFirstTriangle(Ptr p) => p.ReadU32LE(16); | |
| public static uint GetNumTriangles(Ptr p) => p.ReadU32LE(20); | |
| } | |
| /// Zero-copy accessor for an IQM file buffer. | |
| /// Initialize with a Ptr positioned at byte 0 of the file. | |
| public class IqmAccessor | |
| { | |
| Ptr pHeader; | |
| /// Bind this accessor to the given file buffer. | |
| /// p must be positioned at offset 0 (the iqmheader). | |
| public void Initialize!(Ptr p) | |
| { | |
| pHeader = p; | |
| } | |
| public uint GetNumVertexes() => IqmHeader.GetNumVertexes(pHeader); | |
| public uint GetNumVertexArrays() => IqmHeader.GetNumVertexArrays(pHeader); | |
| public uint GetNumMeshes() => IqmHeader.GetNumMeshes(pHeader); | |
| public uint GetNumTriangles() => IqmHeader.GetNumTriangles(pHeader); | |
| /// Returns the index of the vertex array with the given type, or -1 if absent. | |
| public int FindVertexArray(IqmVertexArrayType type) | |
| { | |
| uint n = IqmHeader.GetNumVertexArrays(pHeader); | |
| uint baseOfs = IqmHeader.GetOfsVertexArrays(pHeader); | |
| Ptr() p; | |
| p.Initialize(pHeader.Data, baseOfs); | |
| for (int i = 0; i < n; i++) | |
| { | |
| if (IqmVertexArrayType.FromInt(IqmVertexArray.GetType(p)) == type) | |
| return i; | |
| p.Offset += IqmVertexArray.Length; | |
| } | |
| return -1; | |
| } | |
| /// Returns the total number of float values needed to hold all vertices | |
| /// of the attribute at vaIndex (num_vertexes * component_count). | |
| /// Returns 0 if vaIndex is -1 (not found). | |
| public uint GetAttributeFloatCount(int vaIndex) | |
| { | |
| if (vaIndex < 0) | |
| return 0; | |
| uint vaOffset = IqmHeader.GetOfsVertexArrays(pHeader) + vaIndex * IqmVertexArray.Length; | |
| Ptr() vap; | |
| vap.Initialize(pHeader.Data, vaOffset); | |
| return IqmHeader.GetNumVertexes(pHeader) * IqmVertexArray.GetSize(vap); | |
| } | |
| /// Normalize the attribute at vaIndex to 32-bit floats, writing into dst starting at dstOff. | |
| /// For signed integer formats (Byte, Short) the result is SNORM in [-1, 1]. | |
| /// For unsigned integer formats (UByte, UShort) the result is UNORM in [0, 1]. | |
| /// Float and Half are copied directly. | |
| public void NormalizeAttribute(int vaIndex, float[]! dst, uint dstOff) | |
| { | |
| assert vaIndex >= 0, "vertex array not found"; | |
| uint vaOffset = IqmHeader.GetOfsVertexArrays(pHeader) + vaIndex * IqmVertexArray.Length; | |
| Ptr() vap; | |
| vap.Initialize(pHeader.Data, vaOffset); | |
| IqmFormat format = IqmFormat.FromInt(IqmVertexArray.GetFormat(vap)); | |
| uint components = IqmVertexArray.GetSize(vap); | |
| uint dataOfs = IqmVertexArray.GetOffset(vap); | |
| uint numVertexes = IqmHeader.GetNumVertexes(pHeader); | |
| uint total = numVertexes * components; | |
| Ptr() dp; | |
| dp.Initialize(pHeader.Data, dataOfs); | |
| switch (format) { | |
| case IqmFormat.Float: | |
| AttributeConverter.Float32LECopy(pHeader.Data, dataOfs, dst, dstOff, | |
| components, components * 4, numVertexes); | |
| break; | |
| //case IqmFormat.Half: | |
| // AttributeConverter.Float16LEToFloat32(pHeader.Data, dataOfs, dst, dstOff, total); | |
| // break; | |
| case IqmFormat.Byte: | |
| for (uint i = 0; i < total; i++) | |
| { | |
| int b = dp.ReadU8(0); | |
| if (b >= 128) | |
| b -= 256; | |
| dst[dstOff + i] = b / 127.0; | |
| dp.Offset++; | |
| } | |
| break; | |
| case IqmFormat.UByte: | |
| for (uint i = 0; i < total; i++) | |
| { | |
| dst[dstOff + i] = dp.ReadU8(0) / 255.0; | |
| dp.Offset++; | |
| } | |
| break; | |
| case IqmFormat.Short: | |
| for (uint i = 0; i < total; i++) | |
| { | |
| dst[dstOff + i] = dp.ReadI16LE(0) / 32767.0; | |
| dp.Offset += 2; | |
| } | |
| break; | |
| case IqmFormat.UShort: | |
| for (uint i = 0; i < total; i++) | |
| { | |
| dst[dstOff + i] = dp.ReadU16LE(0) / 65535.0; | |
| dp.Offset += 2; | |
| } | |
| break; | |
| default: | |
| assert false, "unsupported IQM attribute format"; | |
| } | |
| } | |
| /// Read all triangle indices into the provided array. | |
| /// dst must have capacity for num_triangles * 3. | |
| public void GetTriangleIndices(uint[]! dst) | |
| { | |
| uint numTriangles = IqmHeader.GetNumTriangles(pHeader); | |
| uint triangleOffset = IqmHeader.GetOfsTriangles(pHeader); | |
| Ptr() tp; | |
| tp.Initialize(pHeader.Data, triangleOffset); | |
| for (uint i = 0; i < numTriangles; i++) | |
| { | |
| dst[i * 3 + 2] = tp.ReadU32LE(0); | |
| dst[i * 3 + 1] = tp.ReadU32LE(4); | |
| dst[i * 3 + 0] = tp.ReadU32LE(8); | |
| tp.Offset += 12; | |
| } | |
| } | |
| /// Read the bounding box (min/max) from the file into outBounds. | |
| /// outBounds should have capacity for 6 floats: [minX, minY, minZ, maxX, maxY, maxZ]. | |
| public void GetBounds(float[]! outBounds) | |
| { | |
| uint ofsBounds = IqmHeader.GetOfsBounds(pHeader); | |
| if (ofsBounds == 0) | |
| return; | |
| // iqmbounds: float bbmin[3], float bbmax[3] (6 floats total, 24 bytes) | |
| AttributeConverter.Float32LECopy(pHeader.Data, ofsBounds, outBounds, 0, 6, 24, 1); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public static class IqmTest | |
| { | |
| static void AssertClose(float actual, float expected, float epsilon, string label) | |
| { | |
| float diff = actual - expected; | |
| if (diff < 0.0) | |
| diff = -diff; | |
| assert diff < epsilon, label; | |
| } | |
| public static byte[]! GetTestData() | |
| { | |
| // Obtained from: https://github.com/lsalzman/iqm/blob/711fd2ce543cf3927ce687ffc09f9dfcb31e8d53/demo/mrfixit.iqm | |
| return resource<byte[]>("mrfixit.iqm"); | |
| } | |
| public static void Main() | |
| { | |
| byte[]! buf = GetTestData(); | |
| Ptr() p; | |
| p.Initialize(buf, 0); | |
| IqmAccessor() acc; | |
| acc.Initialize(p); | |
| // Header checks | |
| assert acc.GetNumVertexes() == 1861, "num_vertexes"; | |
| assert acc.GetNumVertexArrays() == 6, "num_vertexarrays"; | |
| assert acc.GetNumMeshes() == 2, "num_meshes"; | |
| assert acc.GetNumTriangles() == 2988, "num_triangles"; | |
| // Find position vertex array | |
| int posIdx = acc.FindVertexArray(IqmVertexArrayType.Position); | |
| assert posIdx >= 0, "position VA not found"; | |
| // Verify float count | |
| uint floatCount = acc.GetAttributeFloatCount(posIdx); | |
| assert floatCount == 1861 * 3, "position float count"; | |
| // Absent attribute returns -1 properly: | |
| int colorIdx = acc.FindVertexArray(IqmVertexArrayType.Color); | |
| assert colorIdx < 0, "color VA should be absent"; | |
| // Normalize positions and spot-check first five vertices. | |
| float[]# positions = new float[floatCount]; | |
| long startMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); | |
| acc.NormalizeAttribute(posIdx, positions, 0); | |
| long endMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); | |
| long elapsedMs = endMs - startMs; | |
| // Expected values extracted from demo/mrfixit.iqm (FLOAT LE, offset 1160). | |
| float eps = 0.0001; | |
| AssertClose(positions[0], -0.178719, eps, "v0.x"); | |
| AssertClose(positions[1], -0.857190, eps, "v0.y"); | |
| AssertClose(positions[2], 4.313614, eps, "v0.z"); | |
| AssertClose(positions[3], -0.190296, eps, "v1.x"); | |
| AssertClose(positions[4], -0.835256, eps, "v1.y"); | |
| AssertClose(positions[5], 4.248078, eps, "v1.z"); | |
| AssertClose(positions[6], -0.335791, eps, "v2.x"); | |
| AssertClose(positions[7], -0.709994, eps, "v2.y"); | |
| AssertClose(positions[8], 4.344994, eps, "v2.z"); | |
| AssertClose(positions[9], -0.389321, eps, "v3.x"); | |
| AssertClose(positions[10], -0.732750, eps, "v3.y"); | |
| AssertClose(positions[11], 4.067679, eps, "v3.z"); | |
| AssertClose(positions[12], -0.248709, eps, "v4.x"); | |
| AssertClose(positions[13], -0.848792, eps, "v4.y"); | |
| AssertClose(positions[14], 4.011199, eps, "v4.z"); | |
| // Normalize blend weights (UByte UNORM) as a format smoke-test. | |
| int blendWeightIdx = acc.FindVertexArray(IqmVertexArrayType.BlendWeights); | |
| assert blendWeightIdx >= 0, "blendweights VA not found"; | |
| uint bwCount = acc.GetAttributeFloatCount(blendWeightIdx); | |
| float[]# bw = new float[bwCount]; | |
| acc.NormalizeAttribute(blendWeightIdx, bw, 0); | |
| // Each vertex has 4 blend weights; values in [0, 1]. | |
| for (uint i = 0; i < bwCount; i++) | |
| { | |
| assert bw[i] >= 0.0, "blend weight negative"; | |
| assert bw[i] <= 1.0, "blend weight > 1"; | |
| } | |
| // Success: print benchmark result. | |
| Console.WriteLine($"IQM tests passed. NormalizeAttribute() for positions took {elapsedMs} ms"); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Lightweight byte array accessor with big-endian and little-endian read/write. | |
| public class Ptr | |
| { | |
| public byte[]! Data; | |
| public uint Offset; | |
| /// Initialize this pointer to read from a buffer starting at the given absolute offset. | |
| public Ptr Initialize!(byte[]! bytes, uint startOffset) | |
| { | |
| Data = bytes; | |
| Offset = startOffset; | |
| return this; | |
| } | |
| /// Read a single unsigned byte (0-255) at the given relative offset. | |
| public byte ReadU8(uint relativeOffset) => Data[Offset + relativeOffset]; | |
| // | |
| // Big-endian reads | |
| // | |
| /// Read a signed 16-bit big-endian integer at the given relative offset. | |
| public short ReadI16BE(uint relativeOffset) | |
| { | |
| byte hi = Data[Offset + relativeOffset]; | |
| byte lo = Data[Offset + relativeOffset + 1]; | |
| short num = (hi << 8) | lo; | |
| // sign-extend. | |
| return (num << 16) >> 16; | |
| } | |
| /// Read an unsigned 16-bit big-endian integer at the given relative offset. | |
| public ushort ReadU16BE(uint relativeOffset) | |
| { | |
| byte hi = Data[Offset + relativeOffset]; | |
| byte lo = Data[Offset + relativeOffset + 1]; | |
| return (hi << 8) | lo; | |
| } | |
| /// Read a signed 32-bit big-endian integer at the given relative offset. | |
| public int ReadI32BE(uint relativeOffset) | |
| { | |
| byte b0 = Data[Offset + relativeOffset]; | |
| byte b1 = Data[Offset + relativeOffset + 1]; | |
| byte b2 = Data[Offset + relativeOffset + 2]; | |
| byte b3 = Data[Offset + relativeOffset + 3]; | |
| return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; | |
| } | |
| /// Read an unsigned 32-bit big-endian integer at the given relative offset. | |
| public uint ReadU32BE(uint relativeOffset) | |
| { | |
| byte b0 = Data[Offset + relativeOffset]; | |
| byte b1 = Data[Offset + relativeOffset + 1]; | |
| byte b2 = Data[Offset + relativeOffset + 2]; | |
| byte b3 = Data[Offset + relativeOffset + 3]; | |
| return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; | |
| } | |
| // | |
| // Big-endian writes | |
| // | |
| /// Write a single unsigned byte at the given relative offset. | |
| public void WriteU8!(uint relativeOffset, byte value) | |
| { | |
| Data[Offset + relativeOffset] = value; | |
| } | |
| /// Write an unsigned 16-bit big-endian integer at the given relative offset. | |
| public void WriteU16BE!(uint relativeOffset, ushort value) | |
| { | |
| Data[Offset + relativeOffset] = value >> 8; | |
| Data[Offset + relativeOffset + 1] = value; | |
| } | |
| /// Write an unsigned 32-bit big-endian integer at the given relative offset. | |
| public void WriteU32BE!(uint relativeOffset, uint value) | |
| { | |
| Data[Offset + relativeOffset] = value >> 24; | |
| Data[Offset + relativeOffset + 1] = value >> 16; | |
| Data[Offset + relativeOffset + 2] = value >> 8; | |
| Data[Offset + relativeOffset + 3] = value; | |
| } | |
| // | |
| // Little-endian reads | |
| // | |
| /// Read a signed 16-bit little-endian integer at the given relative offset. | |
| public short ReadI16LE(uint relativeOffset) | |
| { | |
| byte lo = Data[Offset + relativeOffset]; | |
| byte hi = Data[Offset + relativeOffset + 1]; | |
| short num = lo | (hi << 8); | |
| // sign-extend. | |
| return (num << 16) >> 16; | |
| } | |
| /// Read an unsigned 16-bit little-endian integer at the given relative offset. | |
| public ushort ReadU16LE(uint relativeOffset) | |
| { | |
| byte lo = Data[Offset + relativeOffset]; | |
| byte hi = Data[Offset + relativeOffset + 1]; | |
| return lo | (hi << 8); | |
| } | |
| /// Read a signed 32-bit little-endian integer at the given relative offset. | |
| public int ReadI32LE(uint relativeOffset) | |
| { | |
| byte b0 = Data[Offset + relativeOffset]; | |
| byte b1 = Data[Offset + relativeOffset + 1]; | |
| byte b2 = Data[Offset + relativeOffset + 2]; | |
| byte b3 = Data[Offset + relativeOffset + 3]; | |
| return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); | |
| } | |
| /// Read an unsigned 32-bit little-endian integer at the given relative offset. | |
| public uint ReadU32LE(uint relativeOffset) | |
| { | |
| byte b0 = Data[Offset + relativeOffset]; | |
| byte b1 = Data[Offset + relativeOffset + 1]; | |
| byte b2 = Data[Offset + relativeOffset + 2]; | |
| byte b3 = Data[Offset + relativeOffset + 3]; | |
| return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); | |
| } | |
| // | |
| // Little-endian writes | |
| // | |
| /// Write an unsigned 16-bit little-endian integer at the given relative offset. | |
| public void WriteU16LE!(uint relativeOffset, ushort value) | |
| { | |
| Data[Offset + relativeOffset] = value; | |
| Data[Offset + relativeOffset + 1] = value >> 8; | |
| } | |
| /// Write an unsigned 32-bit little-endian integer at the given relative offset. | |
| public void WriteU32LE!(uint relativeOffset, uint value) | |
| { | |
| Data[Offset + relativeOffset] = value; | |
| Data[Offset + relativeOffset + 1] = value >> 8; | |
| Data[Offset + relativeOffset + 2] = value >> 16; | |
| Data[Offset + relativeOffset + 3] = value >> 24; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment