instance-repeated-meshes.mjs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // Lossless grouping: only meshes with byte-identical primitives, attributes and material
  2. // dependencies become instances. Geometry is neither rescaled nor decimated here.
  3. import assert from 'node:assert/strict'
  4. import { createHash } from 'node:crypto'
  5. export function parseGlb(bytes) {
  6. assert.equal(bytes.toString('ascii', 0, 4), 'glTF')
  7. assert.equal(bytes.readUInt32LE(4), 2)
  8. assert.equal(bytes.readUInt32LE(8), bytes.length)
  9. const size = bytes.readUInt32LE(12), offset = 20 + size
  10. assert.equal(bytes.toString('ascii', 16, 20), 'JSON')
  11. assert.equal(bytes.toString('ascii', offset + 4, offset + 8), 'BIN\0')
  12. return { json: JSON.parse(bytes.toString('utf8', 20, offset)), bin: bytes.subarray(offset + 8, offset + 8 + bytes.readUInt32LE(offset)) }
  13. }
  14. export function packGlb(json, bin) {
  15. let text = Buffer.from(JSON.stringify(json))
  16. text = Buffer.concat([text, Buffer.alloc((-text.length) & 3, 0x20)])
  17. bin = Buffer.concat([bin, Buffer.alloc((-bin.length) & 3)])
  18. const header = Buffer.alloc(20), tail = Buffer.alloc(8)
  19. header.write('glTF'); header.writeUInt32LE(2, 4); header.writeUInt32LE(28 + text.length + bin.length, 8)
  20. header.writeUInt32LE(text.length, 12); header.write('JSON', 16)
  21. tail.writeUInt32LE(bin.length); tail.write('BIN\0', 4)
  22. return Buffer.concat([header, text, tail, bin])
  23. }
  24. export function accessorValues(json, bin, index) {
  25. const a = json.accessors[index], view = json.bufferViews[a.bufferView]
  26. assert.ok(!a.sparse && !a.normalized)
  27. const components = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }[a.type]
  28. assert.equal(a.componentType, 5126)
  29. const start = (view.byteOffset || 0) + (a.byteOffset || 0), stride = view.byteStride || components * 4
  30. return Array.from({ length: a.count }, (_, i) => Array.from({ length: components }, (_, c) => bin.readFloatLE(start + i * stride + c * 4)))
  31. }
  32. export function meshSignature(json, bin, mesh) {
  33. function viewData(index) {
  34. const view = json.bufferViews[index], bytes = bin.subarray(view.byteOffset || 0, (view.byteOffset || 0) + view.byteLength)
  35. return createHash('sha256').update(bytes).digest('hex')
  36. }
  37. function attribute(index) {
  38. const a = structuredClone(json.accessors[index]); delete a.name
  39. if (a.bufferView !== undefined) { a.bufferView = viewData(a.bufferView) }
  40. return a
  41. }
  42. return JSON.stringify(mesh.primitives.map(p => ({
  43. mode: p.mode ?? 4, material: json.materials[p.material],
  44. attributes: Object.fromEntries(Object.entries(p.attributes).map(([k, i]) => [k, attribute(i)])),
  45. indices: attribute(p.indices),
  46. extensions: p.extensions?.KHR_draco_mesh_compression ? {
  47. attributes: p.extensions.KHR_draco_mesh_compression.attributes,
  48. payload: viewData(p.extensions.KHR_draco_mesh_compression.bufferView),
  49. } : undefined,
  50. })))
  51. }
  52. export function instanceRepeatedMeshes(bytes) {
  53. const { json, bin } = parseGlb(bytes)
  54. assert.ok(!json.skins?.length && !json.animations?.length)
  55. assert.equal(json.scenes.length,1,'only one static scene supported')
  56. assert.ok(json.accessors.every(a=>!a.sparse),'sparse attributes unsupported')
  57. assert.ok((json.extensionsUsed||[]).every(e=>e==='KHR_draco_mesh_compression'),'unsupported extension')
  58. for(const mesh of json.meshes){
  59. assert.ok(!mesh.weights,'static morph weights unsupported')
  60. for(const p of mesh.primitives){
  61. assert.ok(!p.targets,'morph targets unsupported')
  62. assert.ok(p.indices!==undefined,'indexed meshes required')
  63. assert.ok(Object.keys(p.extensions||{}).every(e=>e==='KHR_draco_mesh_compression'))
  64. }
  65. }
  66. const roots = json.scenes[json.scene || 0].nodes
  67. assert.deepEqual([...roots].sort((a,b) => a-b), json.nodes.map((_, i) => i), 'all source nodes must be scene roots')
  68. const groups = new Map(), originalNodes = json.nodes.length
  69. for (const node of json.nodes) {
  70. assert.ok(node.mesh !== undefined && !node.children?.length && !node.matrix && !node.extensions && !node.weights)
  71. const key = meshSignature(json, bin, json.meshes[node.mesh])
  72. if (!groups.has(key)) groups.set(key, [])
  73. groups.get(key).push(node)
  74. }
  75. const chunks = [bin], nodes = [], meshes = [], records = []
  76. let length = bin.length
  77. function append(values, components) {
  78. const padding = (-length) & 3
  79. if (padding) { chunks.push(Buffer.alloc(padding)); length += padding }
  80. const data = Buffer.alloc(values.length * 4)
  81. values.forEach((v, i) => { assert.ok(Number.isFinite(v)); data.writeFloatLE(v, i * 4) })
  82. const view = json.bufferViews.length
  83. json.bufferViews.push({ buffer: 0, byteOffset: length, byteLength: data.length })
  84. chunks.push(data); length += data.length
  85. const accessor = json.accessors.length
  86. json.accessors.push({ bufferView: view, componentType: 5126, type: components === 4 ? 'VEC4' : 'VEC3', count: values.length / components,
  87. ...(components === 3 ? { min: [0,1,2].map(a => Math.min(...values.filter((_,i)=>i%3===a))), max: [0,1,2].map(a=>Math.max(...values.filter((_,i)=>i%3===a))) } : {}) })
  88. return accessor
  89. }
  90. for (const group of groups.values()) {
  91. const meshIndex = meshes.length
  92. meshes.push(json.meshes[group[0].mesh])
  93. if (group.length === 1) { nodes.push({ ...group[0], mesh: meshIndex }); continue }
  94. const trs = { TRANSLATION: [], ROTATION: [], SCALE: [] }
  95. for (const node of group) {
  96. const t = node.translation || [0,0,0], r = node.rotation || [0,0,0,1], s = node.scale || [1,1,1]
  97. assert.ok(s.every(v => Math.abs(v-1)<1e-7), 'bake nonuniform scale before instancing')
  98. assert.ok(r.slice(0,3).every(v=>Math.abs(v)<1e-7)&&Math.abs(r[3]-1)<1e-7, 'bake rotation before instancing')
  99. trs.TRANSLATION.push(...t); trs.ROTATION.push(...r); trs.SCALE.push(...s)
  100. }
  101. const attributes = { TRANSLATION: append(trs.TRANSLATION, 3) }
  102. nodes.push({ mesh: meshIndex, name: 'Instances_' + meshIndex, extensions: { EXT_mesh_gpu_instancing: { attributes } },
  103. extras: { instanceNames: group.map(n => n.name), instanceExtras: group.map(n => n.extras || {}) } })
  104. records.push({ node: nodes.length - 1, count: group.length, names: group.map(n => n.name) })
  105. }
  106. json.nodes = nodes; json.meshes = meshes; json.scenes[json.scene || 0].nodes = nodes.map((_,i)=>i)
  107. if (records.length) {
  108. json.extensionsUsed = [...new Set([...(json.extensionsUsed || []), 'EXT_mesh_gpu_instancing'])]
  109. json.extensionsRequired = [...new Set([...(json.extensionsRequired || []), 'EXT_mesh_gpu_instancing'])]
  110. }
  111. // Prune mesh/accessor/view closure, including instance attributes; discard duplicate compressed payloads.
  112. const accessorIds = new Set(), viewIds = new Set(), materialIds = new Set()
  113. for (const mesh of meshes) for (const p of mesh.primitives) {
  114. Object.values(p.attributes).forEach(i => accessorIds.add(i)); accessorIds.add(p.indices); materialIds.add(p.material)
  115. if (p.extensions?.KHR_draco_mesh_compression) viewIds.add(p.extensions.KHR_draco_mesh_compression.bufferView)
  116. }
  117. for (const node of nodes) Object.values(node.extensions?.EXT_mesh_gpu_instancing?.attributes || {}).forEach(i => accessorIds.add(i))
  118. for (const i of accessorIds) if (json.accessors[i].bufferView !== undefined) viewIds.add(json.accessors[i].bufferView)
  119. assert.ok(!json.images?.length && !json.textures?.length, 'city asset conversion only supports untextured palette')
  120. const accessors = [...accessorIds].map(i => json.accessors[i]), accessorMap = new Map([...accessorIds].map((i,n)=>[i,n]))
  121. const all = Buffer.concat(chunks), views = [], payloads = []; let offset = 0
  122. const viewMap = new Map()
  123. for (const i of viewIds) {
  124. const v = json.bufferViews[i], padding = (-offset)&3
  125. if (padding) { payloads.push(Buffer.alloc(padding)); offset += padding }
  126. viewMap.set(i, views.length)
  127. views.push({ ...v, byteOffset: offset }); payloads.push(all.subarray(v.byteOffset || 0, (v.byteOffset || 0)+v.byteLength)); offset += v.byteLength
  128. }
  129. for (const a of accessors) if (a.bufferView !== undefined) a.bufferView = viewMap.get(a.bufferView)
  130. const materials = [...materialIds].map(i => json.materials[i]), materialMap = new Map([...materialIds].map((i,n)=>[i,n]))
  131. for (const mesh of meshes) for (const p of mesh.primitives) {
  132. p.attributes = Object.fromEntries(Object.entries(p.attributes).map(([k,i])=>[k,accessorMap.get(i)]))
  133. p.indices = accessorMap.get(p.indices); p.material = materialMap.get(p.material)
  134. if (p.extensions?.KHR_draco_mesh_compression) p.extensions.KHR_draco_mesh_compression.bufferView = viewMap.get(p.extensions.KHR_draco_mesh_compression.bufferView)
  135. }
  136. for (const node of nodes) if (node.extensions?.EXT_mesh_gpu_instancing) node.extensions.EXT_mesh_gpu_instancing.attributes = Object.fromEntries(Object.entries(node.extensions.EXT_mesh_gpu_instancing.attributes).map(([k,i])=>[k,accessorMap.get(i)]))
  137. json.accessors = accessors; json.bufferViews = views; json.materials = materials; json.buffers = [{byteLength:offset}]
  138. return { bytes: packGlb(json, Buffer.concat(payloads)), report: { originalNodes, outputNodes: nodes.length, instanceGroups: records.length,
  139. instancedPlacements: records.reduce((s,g)=>s+g.count,0), records } }
  140. }