Skip to content

Commit 4eeacd1

Browse files
committed
add ability to export to USDZ
1 parent d0c624a commit 4eeacd1

File tree

5 files changed

+1332
-11
lines changed

5 files changed

+1332
-11
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE)
44

5-
A tool for creating macromolecular images and 3D models. It is based on [molrender](https://github.com/molstar/molrender), which is based on [Mol\*](https://github.com/molstar/molstar). It can create PNG, JPEG, OBJ, and GLB files.
5+
A tool for creating macromolecular images and 3D models. It is based on [molrender](https://github.com/molstar/molrender), which is based on [Mol\*](https://github.com/molstar/molstar). It can create PNG, JPEG, OBJ, GLB, and USDZ files.
66

77
The tool is being used on [MolAR](https://stanford.edu/~sukolsak/ar/) to create 3D models of proteins for augmented reality.
88

@@ -19,7 +19,7 @@ The `options` are
1919

2020
--width WIDTH image height
2121
--height HEIGHT image width
22-
--format FORMAT image format (png, jpeg, obj, or glb)
22+
--format FORMAT image format (png, jpeg, obj, glb, or usdz)
2323

2424
For example, the following command will create `6vxx.obj` in the `out` folder. The `6vxx.cif` file (structure data of SARS-CoV-2 spike glycoprotein) can be downloaded from https://files.rcsb.org/download/6vxx.cif.
2525

src/3d-exporter.ts

+64
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
88
import { Color } from 'molstar/lib/mol-util/color/color';
9+
import { ValueType, UsdAttribute, UsdData, CrateFile } from './usdz';
910

1011
function computeBounding(points: Vec3[]) {
1112
const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
@@ -207,3 +208,66 @@ export function exportGlb(meshByColor: Map<Color, Mesh>) {
207208
]);
208209
return glb;
209210
}
211+
212+
export function exportUsdz(meshByColor: Map<Color, Mesh>) {
213+
const root = new UsdData('', '');
214+
const usdObj = root.createChild('ar', 'Xform');
215+
usdObj.metadata['assetInfo'] = {'name': 'ar'};
216+
usdObj.metadata['kind'] = 'component';
217+
218+
const materials_scope = usdObj.createChild('Materials', 'Scope');
219+
220+
let refId = 0;
221+
222+
const vecsToFloatArray = (v: number[][]) => {
223+
const a = new Float32Array(v.length * 3);
224+
for (let i = 0; i < v.length; ++i) {
225+
a[i * 3] = v[i][0];
226+
a[i * 3 + 1] = v[i][1];
227+
a[i * 3 + 2] = v[i][2];
228+
}
229+
return a;
230+
};
231+
232+
meshByColor.forEach((mesh, color) => {
233+
const { positions, normals, faces } = mesh;
234+
const positions2 = vecsToFloatArray(positions);
235+
const normals2 = vecsToFloatArray(normals);
236+
const faceVertexCounts = new Uint32Array(faces.length / 3);
237+
faceVertexCounts.fill(3);
238+
239+
const rgb = new Float32Array(Color.toRgbNormalized(color));
240+
const usdMaterial = materials_scope.createChild('k' + String(refId), 'Material');
241+
242+
const usdShader = usdMaterial.createChild('surfaceShader', 'Shader');
243+
const infoIdAtt = new UsdAttribute('info:id', 'UsdPreviewSurface', ValueType.token, 'token');
244+
infoIdAtt.addQualifier('uniform');
245+
usdShader.addAttribute(infoIdAtt);
246+
usdShader.addAttribute(new UsdAttribute('inputs:diffuseColor', rgb, ValueType.vec3f, 'color3f'));
247+
usdShader.addAttribute(new UsdAttribute('inputs:roughness', 0.2, ValueType.float, 'float'));
248+
const surface = new UsdAttribute('outputs:surface', null, ValueType.token, 'token');
249+
usdShader.addAttribute(surface);
250+
251+
usdMaterial.addAttribute(new UsdAttribute('outputs:surface', surface, ValueType.Invalid, 'token'));
252+
253+
const usdMesh = usdObj.createChild('m' + String(refId), 'Mesh');
254+
usdMesh.addAttribute(new UsdAttribute('material:binding', usdMaterial, ValueType.Invalid, 'rel'));
255+
usdMesh.addAttribute(new UsdAttribute('doubleSided', false, ValueType.bool, 'bool'));
256+
usdMesh.addAttribute(new UsdAttribute('faceVertexCounts', faceVertexCounts, ValueType.int, 'int[]', true));
257+
usdMesh.addAttribute(new UsdAttribute('faceVertexIndices', faces, ValueType.int, 'int[]', true));
258+
usdMesh.addAttribute(new UsdAttribute('points', positions2, ValueType.vec3f, 'point3f[]', true));
259+
const normalsAtt = new UsdAttribute('primvars:normals', normals2, ValueType.vec3f, 'normal3f[]', true);
260+
normalsAtt.metadata['interpolation'] = 'vertex';
261+
usdMesh.addAttribute(normalsAtt);
262+
const subdivAtt = new UsdAttribute('subdivisionScheme', 'none', ValueType.token, 'token');
263+
subdivAtt.addQualifier('uniform');
264+
usdMesh.addAttribute(subdivAtt);
265+
266+
refId++;
267+
});
268+
269+
const crateFile = new CrateFile();
270+
crateFile.writeUsd(root);
271+
const usdz = crateFile.getUsdz();
272+
return Buffer.from(usdz.buffer);
273+
}

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function addBasicArgs(currParser: argparse.ArgumentParser, isDir: boolean) {
4141
});
4242
currParser.addArgument([ '--format' ], {
4343
action: 'store',
44-
help: 'image format (png, jpeg, obj, or glb)',
44+
help: 'image format (png, jpeg, obj, glb, or usdz)',
4545
defaultValue: 'png'
4646
});
4747
}

src/render.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { ColorNames } from 'molstar/lib/mol-util/color/names';
3838
import { Camera } from 'molstar/lib/mol-canvas3d/camera';
3939
import { SyncRuntimeContext } from 'molstar/lib/mol-task/execution/synchronous';
4040
import { AssetManager } from 'molstar/lib/mol-util/assets';
41-
import { Mesh, exportObj, exportGlb } from './3d-exporter';
41+
import { Mesh, exportObj, exportGlb, exportUsdz } from './3d-exporter';
4242

4343
/**
4444
* Helper method to create PNG with given PNG data
@@ -65,9 +65,9 @@ async function writeObjFile(obj: string, objOutPath: string, mtl: string, mtlOut
6565
})
6666
]);
6767
}
68-
async function writeGlbFile(glb: Buffer, outPath: string) {
68+
async function writeFile(data: Buffer, outPath: string) {
6969
await new Promise<void>(resolve => {
70-
fs.writeFile(outPath, glb, () => resolve());
70+
fs.writeFile(outPath, data, () => resolve());
7171
});
7272
}
7373

@@ -157,7 +157,7 @@ export class ImageRenderer {
157157
imagePass: ImagePass
158158
assetManager = new AssetManager()
159159

160-
constructor(private width: number, private height: number, private format: 'png' | 'jpeg' | 'obj' | 'glb') {
160+
constructor(private width: number, private height: number, private format: 'png' | 'jpeg' | 'obj' | 'glb' | 'usdz') {
161161
this.gl = getGLContext(this.width, this.height, {
162162
alpha: false,
163163
antialias: true,
@@ -261,7 +261,7 @@ export class ImageRenderer {
261261
}
262262

263263
/**
264-
* Creates OBJ/GLB with the current 3dcanvas data
264+
* Creates OBJ/GLB/USDZ with the current 3dcanvas data
265265
*/
266266
async create3DModel(outPath: string) {
267267
this.canvas3d.commit(true);
@@ -394,7 +394,7 @@ export class ImageRenderer {
394394
});
395395
const translate = Vec3.create(-(box.min[0] + box.max[0]) / 2, -box.min[1], -(box.min[2] + box.max[2]) / 2);
396396
const size = Math.max(box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]);
397-
const scale = (this.format === 'glb') ? Math.min(0.4 / size, 0.01) : 1;
397+
const scale = (this.format === 'glb') ? Math.min(0.4 / size, 0.01) : (this.format === 'usdz') ? Math.min(40 / size, 1) : 1;
398398
meshByColor.forEach(mesh => {
399399
for (const position of mesh.positions) {
400400
Vec3.add(position, position, translate);
@@ -409,12 +409,15 @@ export class ImageRenderer {
409409
await writeObjFile(obj, `${outPath}.obj`, mtl, `${outPath}.mtl`);
410410
} else if (this.format === 'glb') {
411411
const glb = exportGlb(meshByColor);
412-
await writeGlbFile(glb, `${outPath}.glb`);
412+
await writeFile(glb, `${outPath}.glb`);
413+
} else if (this.format === 'usdz') {
414+
const usdz = exportUsdz(meshByColor);
415+
await writeFile(usdz, `${outPath}.usdz`);
413416
}
414417
}
415418

416419
async createImage(outPath: string, size: StructureSize) {
417-
if (this.format === 'obj' || this.format === 'glb') {
420+
if (this.format === 'obj' || this.format === 'glb' || this.format === 'usdz') {
418421
await this.create3DModel(outPath);
419422
return;
420423
}

0 commit comments

Comments
 (0)