glb-exporter.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /**
  2. * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { asciiWrite } from '../../mol-io/common/ascii';
  8. import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
  9. import { Box3D } from '../../mol-math/geometry';
  10. import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
  11. import { PLUGIN_VERSION } from '../../mol-plugin/version';
  12. import { RuntimeContext } from '../../mol-task';
  13. import { Color } from '../../mol-util/color/color';
  14. import { fillSerial } from '../../mol-util/array';
  15. import { NumberArray } from '../../mol-util/type-helpers';
  16. import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
  17. // avoiding namespace lookup improved performance in Chrome (Aug 2020)
  18. const v3fromArray = Vec3.fromArray;
  19. const v3normalize = Vec3.normalize;
  20. const v3toArray = Vec3.toArray;
  21. // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0
  22. const UNSIGNED_BYTE = 5121;
  23. const UNSIGNED_INT = 5125;
  24. const FLOAT = 5126;
  25. const ARRAY_BUFFER = 34962;
  26. const ELEMENT_ARRAY_BUFFER = 34963;
  27. const GLTF_MAGIC_BYTE = 0x46546C67;
  28. const JSON_CHUNK_TYPE = 0x4E4F534A;
  29. const BIN_CHUNK_TYPE = 0x004E4942;
  30. const JSON_PAD_CHAR = 0x20;
  31. const BIN_PAD_CHAR = 0x00;
  32. export type GlbData = {
  33. glb: Uint8Array
  34. }
  35. export class GlbExporter extends MeshExporter<GlbData> {
  36. readonly fileExtension = 'glb';
  37. private nodes: Record<string, any>[] = [];
  38. private meshes: Record<string, any>[] = [];
  39. private materials: Record<string, any>[] = [];
  40. private materialMap = new Map<string, number>();
  41. private accessors: Record<string, any>[] = [];
  42. private bufferViews: Record<string, any>[] = [];
  43. private binaryBuffer: ArrayBuffer[] = [];
  44. private byteOffset = 0;
  45. private centerTransform: Mat4;
  46. private static vec3MinMax(a: NumberArray) {
  47. const min: number[] = [Infinity, Infinity, Infinity];
  48. const max: number[] = [-Infinity, -Infinity, -Infinity];
  49. for (let i = 0, il = a.length; i < il; i += 3) {
  50. for (let j = 0; j < 3; ++j) {
  51. min[j] = Math.min(a[i + j], min[j]);
  52. max[j] = Math.max(a[i + j], max[j]);
  53. }
  54. }
  55. return [min, max];
  56. }
  57. private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
  58. this.binaryBuffer.push(buffer);
  59. const bufferViewOffset = this.bufferViews.length;
  60. this.bufferViews.push({
  61. buffer: 0,
  62. byteOffset: this.byteOffset,
  63. byteLength: buffer.byteLength,
  64. target
  65. });
  66. this.byteOffset += buffer.byteLength;
  67. const accessorOffset = this.accessors.length;
  68. this.accessors.push({
  69. bufferView: bufferViewOffset,
  70. byteOffset: 0,
  71. componentType,
  72. count,
  73. type,
  74. min,
  75. max,
  76. normalized
  77. });
  78. return accessorOffset;
  79. }
  80. private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
  81. const tmpV = Vec3();
  82. const stride = isGeoTexture ? 4 : 3;
  83. const vertexArray = new Float32Array(vertexCount * 3);
  84. const normalArray = new Float32Array(vertexCount * 3);
  85. let indexArray: Uint32Array | undefined;
  86. // position
  87. for (let i = 0; i < vertexCount; ++i) {
  88. v3fromArray(tmpV, vertices, i * stride);
  89. v3toArray(tmpV, vertexArray, i * 3);
  90. }
  91. // normal
  92. for (let i = 0; i < vertexCount; ++i) {
  93. v3fromArray(tmpV, normals, i * stride);
  94. v3normalize(tmpV, tmpV);
  95. v3toArray(tmpV, normalArray, i * 3);
  96. }
  97. // face
  98. if (!isGeoTexture) {
  99. indexArray = indices!.slice(0, drawCount);
  100. }
  101. const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
  102. let vertexBuffer = vertexArray.buffer;
  103. let normalBuffer = normalArray.buffer;
  104. let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
  105. if (!IsNativeEndianLittle) {
  106. vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
  107. normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
  108. if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
  109. }
  110. return {
  111. vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
  112. normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
  113. indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
  114. };
  115. }
  116. private addColorBuffer(geoData: MeshGeoData, interpolatedColors: Uint8Array | undefined, interpolatedOverpaint: Uint8Array | undefined, interpolatedTransparency: Uint8Array | undefined) {
  117. const { values, vertexCount } = geoData;
  118. const uAlpha = values.uAlpha.ref.value;
  119. const colorArray = new Uint8Array(vertexCount * 4);
  120. for (let i = 0; i < vertexCount; ++i) {
  121. let color = GlbExporter.getColor(i, geoData, interpolatedColors, interpolatedOverpaint);
  122. const transparency = GlbExporter.getTransparency(i, geoData, interpolatedTransparency);
  123. const alpha = uAlpha * (1 - transparency);
  124. color = Color.sRGBToLinear(color);
  125. Color.toArray(color, colorArray, i * 4);
  126. colorArray[i * 4 + 3] = Math.round(alpha * 255);
  127. }
  128. let colorBuffer = colorArray.buffer;
  129. if (!IsNativeEndianLittle) {
  130. colorBuffer = flipByteOrder(new Uint8Array(colorBuffer), 4);
  131. }
  132. return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
  133. }
  134. private addMaterial(metalness: number, roughness: number) {
  135. const hash = `${metalness}|${roughness}`;
  136. if (!this.materialMap.has(hash)) {
  137. this.materialMap.set(hash, this.materials.length);
  138. this.materials.push({
  139. pbrMetallicRoughness: {
  140. baseColorFactor: [1, 1, 1, 1],
  141. metallicFactor: metalness,
  142. roughnessFactor: roughness
  143. }
  144. });
  145. }
  146. return this.materialMap.get(hash)!;
  147. }
  148. protected async addMeshWithColors(input: AddMeshInput) {
  149. const { mesh, values, isGeoTexture, webgl, ctx } = input;
  150. const t = Mat4();
  151. const colorType = values.dColorType.ref.value;
  152. const overpaintType = values.dOverpaintType.ref.value;
  153. const transparencyType = values.dTransparencyType.ref.value;
  154. const dTransparency = values.dTransparency.ref.value;
  155. const aTransform = values.aTransform.ref.value;
  156. const instanceCount = values.uInstanceCount.ref.value;
  157. const metalness = values.uMetalness.ref.value;
  158. const roughness = values.uRoughness.ref.value;
  159. const material = this.addMaterial(metalness, roughness);
  160. let interpolatedColors: Uint8Array | undefined;
  161. if (colorType === 'volume' || colorType === 'volumeInstance') {
  162. const stride = isGeoTexture ? 4 : 3;
  163. interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
  164. }
  165. let interpolatedOverpaint: Uint8Array | undefined;
  166. if (overpaintType === 'volumeInstance') {
  167. const stride = isGeoTexture ? 4 : 3;
  168. interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
  169. }
  170. let interpolatedTransparency: Uint8Array | undefined;
  171. if (transparencyType === 'volumeInstance') {
  172. const stride = isGeoTexture ? 4 : 3;
  173. interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
  174. }
  175. // instancing
  176. const sameGeometryBuffers = mesh !== undefined;
  177. const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
  178. let vertexAccessorIndex: number;
  179. let normalAccessorIndex: number;
  180. let indexAccessorIndex: number | undefined;
  181. let colorAccessorIndex: number;
  182. let meshIndex: number;
  183. await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
  184. for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
  185. if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
  186. // create a glTF mesh if needed
  187. if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
  188. const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
  189. // create geometry buffers if needed
  190. if (instanceIndex === 0 || !sameGeometryBuffers) {
  191. const accessorIndices = this.addGeometryBuffers(vertices, normals, indices, vertexCount, drawCount, isGeoTexture);
  192. vertexAccessorIndex = accessorIndices.vertexAccessorIndex;
  193. normalAccessorIndex = accessorIndices.normalAccessorIndex;
  194. indexAccessorIndex = accessorIndices.indexAccessorIndex;
  195. }
  196. // create a color buffer if needed
  197. if (instanceIndex === 0 || !sameColorBuffer) {
  198. colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
  199. }
  200. // glTF mesh
  201. meshIndex = this.meshes.length;
  202. this.meshes.push({
  203. primitives: [{
  204. attributes: {
  205. POSITION: vertexAccessorIndex!,
  206. NORMAL: normalAccessorIndex!,
  207. COLOR_0: colorAccessorIndex!
  208. },
  209. indices: indexAccessorIndex,
  210. material
  211. }]
  212. });
  213. }
  214. // node
  215. Mat4.fromArray(t, aTransform, instanceIndex * 16);
  216. Mat4.mul(t, this.centerTransform, t);
  217. const node: Record<string, any> = {
  218. mesh: meshIndex!,
  219. matrix: t.slice()
  220. };
  221. this.nodes.push(node);
  222. }
  223. }
  224. async getData() {
  225. const binaryBufferLength = this.byteOffset;
  226. const gltf = {
  227. asset: {
  228. version: '2.0',
  229. generator: `Mol* ${PLUGIN_VERSION}`
  230. },
  231. scenes: [{
  232. nodes: fillSerial(new Array(this.nodes.length) as number[])
  233. }],
  234. nodes: this.nodes,
  235. meshes: this.meshes,
  236. buffers: [{
  237. byteLength: binaryBufferLength,
  238. }],
  239. bufferViews: this.bufferViews,
  240. accessors: this.accessors,
  241. materials: this.materials
  242. };
  243. const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
  244. let padding = null;
  245. if (byteLength % 4 !== 0) {
  246. const pad = 4 - (byteLength % 4);
  247. byteLength += pad;
  248. padding = new Uint8Array(pad);
  249. padding.fill(padChar);
  250. }
  251. const preamble = new ArrayBuffer(8);
  252. const preambleDataView = new DataView(preamble);
  253. preambleDataView.setUint32(0, byteLength, true);
  254. preambleDataView.setUint32(4, chunkType, true);
  255. const chunk = [preamble, ...data];
  256. if (padding) {
  257. chunk.push(padding.buffer);
  258. }
  259. return [chunk, 8 + byteLength];
  260. };
  261. const jsonString = JSON.stringify(gltf);
  262. const jsonBuffer = new Uint8Array(jsonString.length);
  263. asciiWrite(jsonBuffer, jsonString);
  264. const [jsonChunk, jsonChunkLength] = createChunk(JSON_CHUNK_TYPE, [jsonBuffer.buffer], jsonBuffer.length, JSON_PAD_CHAR);
  265. const [binaryChunk, binaryChunkLength] = createChunk(BIN_CHUNK_TYPE, this.binaryBuffer, binaryBufferLength, BIN_PAD_CHAR);
  266. const glbBufferLength = 12 + jsonChunkLength + binaryChunkLength;
  267. const header = new ArrayBuffer(12);
  268. const headerDataView = new DataView(header);
  269. headerDataView.setUint32(0, GLTF_MAGIC_BYTE, true); // magic number "glTF"
  270. headerDataView.setUint32(4, 2, true); // version
  271. headerDataView.setUint32(8, glbBufferLength, true); // length
  272. const glbBuffer = [header, ...jsonChunk, ...binaryChunk];
  273. const glb = new Uint8Array(glbBufferLength);
  274. let offset = 0;
  275. for (const buffer of glbBuffer) {
  276. glb.set(new Uint8Array(buffer), offset);
  277. offset += buffer.byteLength;
  278. }
  279. return { glb };
  280. }
  281. async getBlob(ctx: RuntimeContext) {
  282. return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
  283. }
  284. constructor(boundingBox: Box3D) {
  285. super();
  286. const tmpV = Vec3();
  287. Vec3.add(tmpV, boundingBox.min, boundingBox.max);
  288. Vec3.scale(tmpV, tmpV, -0.5);
  289. this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
  290. }
  291. }