cione-comp-lib
Version:
`$ yarn add cione-comp-lib` 或者 `$ npm i cione-comp-lib -S`
412 lines (396 loc) • 15.3 kB
JavaScript
import { DoubleSide } from "../../../build/three.module.mjs";
import { strToU8, zipSync } from "../libs/fflate.module.mjs";
class USDZExporter {
async parse(scene, options = {}) {
options = Object.assign({
ar: {
anchoring: { type: "plane" },
planeAnchoring: { alignment: "horizontal" }
}
}, options);
const files = {};
const modelFileName = "model.usda";
files[modelFileName] = null;
let output = buildHeader();
output += buildSceneStart(options);
const materials = {};
const textures = {};
scene.traverseVisible((object) => {
if (object.isMesh) {
const geometry = object.geometry;
const material = object.material;
if (material.isMeshStandardMaterial) {
const geometryFileName = "geometries/Geometry_" + geometry.id + ".usd";
if (!(geometryFileName in files)) {
const meshObject = buildMeshObject(geometry);
files[geometryFileName] = buildUSDFileAsString(meshObject);
}
if (!(material.uuid in materials)) {
materials[material.uuid] = material;
}
output += buildXform(object, geometry, material);
} else {
console.warn("THREE.USDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)", object);
}
} else if (object.isCamera) {
output += buildCamera(object);
}
});
output += buildSceneEnd();
output += buildMaterials(materials, textures);
files[modelFileName] = strToU8(output);
output = null;
for (const id in textures) {
const texture = textures[id];
const color = id.split("_")[1];
const isRGBA = texture.format === 1023;
const canvas = imageToCanvas(texture.image, color);
const blob = await new Promise((resolve) => canvas.toBlob(resolve, isRGBA ? "image/png" : "image/jpeg", 1));
files[`textures/Texture_${id}.${isRGBA ? "png" : "jpg"}`] = new Uint8Array(await blob.arrayBuffer());
}
let offset = 0;
for (const filename in files) {
const file = files[filename];
const headerSize = 34 + filename.length;
offset += headerSize;
const offsetMod64 = offset & 63;
if (offsetMod64 !== 4) {
const padLength = 64 - offsetMod64;
const padding = new Uint8Array(padLength);
files[filename] = [file, { extra: { 12345: padding } }];
}
offset = file.length;
}
return zipSync(files, { level: 0 });
}
}
function imageToCanvas(image, color) {
if (typeof HTMLImageElement !== "undefined" && image instanceof HTMLImageElement || typeof HTMLCanvasElement !== "undefined" && image instanceof HTMLCanvasElement || typeof OffscreenCanvas !== "undefined" && image instanceof OffscreenCanvas || typeof ImageBitmap !== "undefined" && image instanceof ImageBitmap) {
const scale = 1024 / Math.max(image.width, image.height);
const canvas = document.createElement("canvas");
canvas.width = image.width * Math.min(1, scale);
canvas.height = image.height * Math.min(1, scale);
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0, canvas.width, canvas.height);
if (color !== void 0) {
const hex = parseInt(color, 16);
const r = (hex >> 16 & 255) / 255;
const g = (hex >> 8 & 255) / 255;
const b = (hex & 255) / 255;
const imagedata = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imagedata.data;
for (let i = 0; i < data.length; i += 4) {
data[i + 0] = data[i + 0] * r;
data[i + 1] = data[i + 1] * g;
data[i + 2] = data[i + 2] * b;
}
context.putImageData(imagedata, 0, 0);
}
return canvas;
} else {
throw new Error("THREE.USDZExporter: No valid image data found. Unable to process texture.");
}
}
const PRECISION = 7;
function buildHeader() {
return `#usda 1.0
(
customLayerData = {
string creator = "Three.js USDZExporter"
}
metersPerUnit = 1
upAxis = "Y"
)
`;
}
function buildSceneStart(options) {
return `def Xform "Root"
{
def Scope "Scenes" (
kind = "sceneLibrary"
)
{
def Xform "Scene" (
customData = {
bool preliminary_collidesWithEnvironment = 0
string sceneName = "Scene"
}
sceneName = "Scene"
)
{
token preliminary:anchoring:type = "${options.ar.anchoring.type}"
token preliminary:planeAnchoring:alignment = "${options.ar.planeAnchoring.alignment}"
`;
}
function buildSceneEnd() {
return `
}
}
}
`;
}
function buildUSDFileAsString(dataToInsert) {
let output = buildHeader();
output += dataToInsert;
return strToU8(output);
}
function buildXform(object, geometry, material) {
const name = "Object_" + object.id;
const transform = buildMatrix(object.matrixWorld);
if (object.matrixWorld.determinant() < 0) {
console.warn("THREE.USDZExporter: USDZ does not support negative scales", object);
}
return `def Xform "${name}" (
prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
)
{
matrix4d xformOp:transform = ${transform}
uniform token[] xformOpOrder = ["xformOp:transform"]
rel material:binding = </Materials/Material_${material.id}>
}
`;
}
function buildMatrix(matrix) {
const array = matrix.elements;
return `( ${buildMatrixRow(array, 0)}, ${buildMatrixRow(array, 4)}, ${buildMatrixRow(array, 8)}, ${buildMatrixRow(array, 12)} )`;
}
function buildMatrixRow(array, offset) {
return `(${array[offset + 0]}, ${array[offset + 1]}, ${array[offset + 2]}, ${array[offset + 3]})`;
}
function buildMeshObject(geometry) {
const mesh = buildMesh(geometry);
return `
def "Geometry"
{
${mesh}
}
`;
}
function buildMesh(geometry) {
const name = "Geometry";
const attributes = geometry.attributes;
const count = attributes.position.count;
return `
def Mesh "${name}"
{
int[] faceVertexCounts = [${buildMeshVertexCount(geometry)}]
int[] faceVertexIndices = [${buildMeshVertexIndices(geometry)}]
normal3f[] normals = [${buildVector3Array(attributes.normal, count)}] (
interpolation = "vertex"
)
point3f[] points = [${buildVector3Array(attributes.position, count)}]
float2[] primvars:st = [${buildVector2Array(attributes.uv, count)}] (
interpolation = "vertex"
)
uniform token subdivisionScheme = "none"
}
`;
}
function buildMeshVertexCount(geometry) {
const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
return Array(count / 3).fill(3).join(", ");
}
function buildMeshVertexIndices(geometry) {
const index = geometry.index;
const array = [];
if (index !== null) {
for (let i = 0; i < index.count; i++) {
array.push(index.getX(i));
}
} else {
const length = geometry.attributes.position.count;
for (let i = 0; i < length; i++) {
array.push(i);
}
}
return array.join(", ");
}
function buildVector3Array(attribute, count) {
if (attribute === void 0) {
console.warn("USDZExporter: Normals missing.");
return Array(count).fill("(0, 0, 0)").join(", ");
}
const array = [];
for (let i = 0; i < attribute.count; i++) {
const x = attribute.getX(i);
const y = attribute.getY(i);
const z = attribute.getZ(i);
array.push(`(${x.toPrecision(PRECISION)}, ${y.toPrecision(PRECISION)}, ${z.toPrecision(PRECISION)})`);
}
return array.join(", ");
}
function buildVector2Array(attribute, count) {
if (attribute === void 0) {
console.warn("USDZExporter: UVs missing.");
return Array(count).fill("(0, 0)").join(", ");
}
const array = [];
for (let i = 0; i < attribute.count; i++) {
const x = attribute.getX(i);
const y = attribute.getY(i);
array.push(`(${x.toPrecision(PRECISION)}, ${1 - y.toPrecision(PRECISION)})`);
}
return array.join(", ");
}
function buildMaterials(materials, textures) {
const array = [];
for (const uuid in materials) {
const material = materials[uuid];
array.push(buildMaterial(material, textures));
}
return `def "Materials"
{
${array.join("")}
}
`;
}
function buildMaterial(material, textures) {
const pad = " ";
const inputs = [];
const samplers = [];
function buildTexture(texture, mapType, color) {
const id = texture.id + (color ? "_" + color.getHexString() : "");
const isRGBA = texture.format === 1023;
textures[id] = texture;
return `
def Shader "Transform2d_${mapType}" (
sdrMetadata = {
string role = "math"
}
)
{
uniform token info:id = "UsdTransform2d"
float2 inputs:in.connect = </Materials/Material_${material.id}/uvReader_st.outputs:result>
float2 inputs:scale = ${buildVector2(texture.repeat)}
float2 inputs:translation = ${buildVector2(texture.offset)}
float2 outputs:result
}
def Shader "Texture_${texture.id}_${mapType}"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @textures/Texture_${id}.${isRGBA ? "png" : "jpg"}@
float2 inputs:st.connect = </Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>
token inputs:wrapS = "repeat"
token inputs:wrapT = "repeat"
float outputs:r
float outputs:g
float outputs:b
float3 outputs:rgb
${material.transparent || material.alphaTest > 0 ? "float outputs:a" : ""}
}`;
}
if (material.side === DoubleSide) {
console.warn("THREE.USDZExporter: USDZ does not support double sided materials", material);
}
if (material.map !== null) {
inputs.push(`${pad}color3f inputs:diffuseColor.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>`);
if (material.transparent) {
inputs.push(`${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>`);
} else if (material.alphaTest > 0) {
inputs.push(`${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>`);
inputs.push(`${pad}float inputs:opacityThreshold = ${material.alphaTest}`);
}
samplers.push(buildTexture(material.map, "diffuse", material.color));
} else {
inputs.push(`${pad}color3f inputs:diffuseColor = ${buildColor(material.color)}`);
}
if (material.emissiveMap !== null) {
inputs.push(`${pad}color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>`);
samplers.push(buildTexture(material.emissiveMap, "emissive"));
} else if (material.emissive.getHex() > 0) {
inputs.push(`${pad}color3f inputs:emissiveColor = ${buildColor(material.emissive)}`);
}
if (material.normalMap !== null) {
inputs.push(`${pad}normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>`);
samplers.push(buildTexture(material.normalMap, "normal"));
}
if (material.aoMap !== null) {
inputs.push(`${pad}float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>`);
samplers.push(buildTexture(material.aoMap, "occlusion"));
}
if (material.roughnessMap !== null && material.roughness === 1) {
inputs.push(`${pad}float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>`);
samplers.push(buildTexture(material.roughnessMap, "roughness"));
} else {
inputs.push(`${pad}float inputs:roughness = ${material.roughness}`);
}
if (material.metalnessMap !== null && material.metalness === 1) {
inputs.push(`${pad}float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>`);
samplers.push(buildTexture(material.metalnessMap, "metallic"));
} else {
inputs.push(`${pad}float inputs:metallic = ${material.metalness}`);
}
if (material.alphaMap !== null) {
inputs.push(`${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>`);
inputs.push(`${pad}float inputs:opacityThreshold = 0.0001`);
samplers.push(buildTexture(material.alphaMap, "opacity"));
} else {
inputs.push(`${pad}float inputs:opacity = ${material.opacity}`);
}
if (material.isMeshPhysicalMaterial) {
inputs.push(`${pad}float inputs:clearcoat = ${material.clearcoat}`);
inputs.push(`${pad}float inputs:clearcoatRoughness = ${material.clearcoatRoughness}`);
inputs.push(`${pad}float inputs:ior = ${material.ior}`);
}
return `
def Material "Material_${material.id}"
{
def Shader "PreviewSurface"
{
uniform token info:id = "UsdPreviewSurface"
${inputs.join("\n")}
int inputs:useSpecularWorkflow = 0
token outputs:surface
}
token outputs:surface.connect = </Materials/Material_${material.id}/PreviewSurface.outputs:surface>
token inputs:frame:stPrimvarName = "st"
def Shader "uvReader_st"
{
uniform token info:id = "UsdPrimvarReader_float2"
token inputs:varname.connect = </Materials/Material_${material.id}.inputs:frame:stPrimvarName>
float2 inputs:fallback = (0.0, 0.0)
float2 outputs:result
}
${samplers.join("\n")}
}
`;
}
function buildColor(color) {
return `(${color.r}, ${color.g}, ${color.b})`;
}
function buildVector2(vector) {
return `(${vector.x}, ${vector.y})`;
}
function buildCamera(camera) {
const name = camera.name ? camera.name : "Camera_" + camera.id;
const transform = buildMatrix(camera.matrixWorld);
if (camera.matrixWorld.determinant() < 0) {
console.warn("THREE.USDZExporter: USDZ does not support negative scales", camera);
}
if (camera.isOrthographicCamera) {
return `def Camera "${name}"
{
matrix4d xformOp:transform = ${transform}
uniform token[] xformOpOrder = ["xformOp:transform"]
float2 clippingRange = (${camera.near.toPrecision(PRECISION)}, ${camera.far.toPrecision(PRECISION)})
float horizontalAperture = ${((Math.abs(camera.left) + Math.abs(camera.right)) * 10).toPrecision(PRECISION)}
float verticalAperture = ${((Math.abs(camera.top) + Math.abs(camera.bottom)) * 10).toPrecision(PRECISION)}
token projection = "orthographic"
}
`;
} else {
return `def Camera "${name}"
{
matrix4d xformOp:transform = ${transform}
uniform token[] xformOpOrder = ["xformOp:transform"]
float2 clippingRange = (${camera.near.toPrecision(PRECISION)}, ${camera.far.toPrecision(PRECISION)})
float focalLength = ${camera.getFocalLength().toPrecision(PRECISION)}
float focusDistance = ${camera.focus.toPrecision(PRECISION)}
float horizontalAperture = ${camera.getFilmWidth().toPrecision(PRECISION)}
token projection = "perspective"
float verticalAperture = ${camera.getFilmHeight().toPrecision(PRECISION)}
}
`;
}
}
export { USDZExporter };