@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
408 lines (376 loc) • 14.8 kB
text/typescript
import {BaseSopOperation} from './_Base';
import {CoreGroup} from '../../../core/geometry/Group';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {BufferAttribute, Mesh, Float32BufferAttribute, Vector3, Line3} from 'three';
import {DefaultOperationParams} from '../../../core/operations/_Base';
import {XAtlasLoaderHandler} from '../../../core/loader/geometry/XAtlas';
import {TypeAssert} from '../../poly/Assert';
import {Potpack, PotPackBox, PotPackBoxResult} from '../../../core/libs/Potpack';
import {LIBRARY_INSTALL_HINT} from '../../../core/loader/common';
import {DEFAULT_UV_LIGHT_MAP_ATTRIB_NAME} from '../../nodes/cop/utils/lightMap/LightMapMaterial';
import {UVUnwrapper} from 'xatlas-three';
// import { PolyEngine } from '../../Poly';
// import {UV_LIGHT_MAP_FLIPPED_ATTRIB_NAME} from '../../nodes/cop/utils/lightMap/LightMapMaterial';
export enum UvUnwrapMethod {
POTPACK = 'potpack',
XATLAS = 'xatlas',
// XATLAS_2 = 'xatlas 2',
}
export const UV_UNWRAP_METHODS: UvUnwrapMethod[] = [
UvUnwrapMethod.XATLAS,
// UvUnwrapMethod.XATLAS_2,
UvUnwrapMethod.POTPACK,
];
interface UvUnwrapSopParams extends DefaultOperationParams {
method: number;
uv: string;
resolution: number;
padding: number;
}
const v1 = new Vector3();
const v2 = new Vector3();
const v3 = new Vector3();
const vMid = new Vector3();
const vEnd = new Vector3();
const line = new Line3();
// const _uvTriangle = new Triangle();
// const _uvTriangleN = new Vector3();
export class UvUnwrapSopOperation extends BaseSopOperation {
static override readonly DEFAULT_PARAMS: UvUnwrapSopParams = {
method: UV_UNWRAP_METHODS.indexOf(UvUnwrapMethod.XATLAS),
uv: DEFAULT_UV_LIGHT_MAP_ATTRIB_NAME,
resolution: 2048,
padding: 4,
};
static override readonly INPUT_CLONED_STATE = InputCloneMode.FROM_NODE;
static override type(): Readonly<SopType.UV_UNWRAP> {
return SopType.UV_UNWRAP;
}
override async cook(inputCoreGroups: CoreGroup[], params: UvUnwrapSopParams) {
const method = UV_UNWRAP_METHODS[params.method];
switch (method) {
case UvUnwrapMethod.XATLAS: {
// return await this._unwrapMeshUVsWithXAtlas(inputCoreGroups, params);
return await this._unwrapMeshUVsWithXAtlas2(inputCoreGroups, params);
}
// case UvUnwrapMethod.XATLAS_2: {
// return await this._unwrapMeshUVsWithXAtlas2(inputCoreGroups, params);
// }
case UvUnwrapMethod.POTPACK: {
return this._unwrapMeshUVsWithPotpack(inputCoreGroups, params);
}
}
TypeAssert.unreachable(method);
}
private async _unwrapMeshUVsWithXAtlas2(inputCoreGroups: CoreGroup[], params: UvUnwrapSopParams) {
const coreGroup = inputCoreGroups[0];
const unwrapper = new UVUnwrapper({BufferAttribute: BufferAttribute});
unwrapper.chartOptions = {
fixWinding: false,
maxBoundaryLength: 0,
maxChartArea: 0,
maxCost: 2,
maxIterations: 1,
normalDeviationWeight: 2,
normalSeamWeight: 4,
roundnessWeight: 0.009999999776482582,
straightnessWeight: 6,
textureSeamWeight: 0.5,
useInputMeshUvs: false,
};
unwrapper.packOptions = {
bilinear: true,
blockAlign: true,
bruteForce: false,
createImage: false,
maxChartSize: 0,
padding: params.padding,
resolution: params.resolution,
rotateCharts: true,
rotateChartsToAxis: true,
texelsPerUnit: 0,
};
if (!this._node) {
this.states?.error.set('no node');
return coreGroup;
}
const xatlasData = await XAtlasLoaderHandler.loadWasm(this._node);
if (!xatlasData) {
this.states?.error.set(`failed to load xatlas. Make sure this is installed. ${LIBRARY_INSTALL_HINT}`);
return coreGroup;
}
await unwrapper.loadLibrary(
(mode, progress) => {
// console.log(mode, progress);
},
xatlasData.wasm, //'https://cdn.jsdelivr.net/npm/xatlasjs@0.1.0/dist/xatlas.wasm',
xatlasData.js //'https://cdn.jsdelivr.net/npm/xatlasjs@0.1.0/dist/xatlas.js'
); // Make sure to wait for the library to load before unwrapping.
const objects = coreGroup.threejsObjectsWithGeo();
for (let object of objects) {
const mesh = object as Mesh;
if (mesh.isMesh) {
// unwrapper.useNormals = true;
// const res = await unwrapper.unwrapGeometry(mesh.geometry);
// mesh.geometry = res[0];
// unwrapper.(mesh.geometry);
await unwrapper.packAtlas([mesh.geometry], params.uv as 'uv');
}
}
return coreGroup;
}
// private async _unwrapMeshUVsWithXAtlas(inputCoreGroups: CoreGroup[], params: UvUnwrapSopParams) {
// const coreGroup = inputCoreGroups[0];
// if (!this._node) {
// return coreGroup;
// }
// const xatlas = await XAtlasLoaderHandler.xatlas(this._node);
// if (!xatlas) {
// this.states?.error.set(`failed to load xatlas. Make sure this is installed. ${LIBRARY_INSTALL_HINT}`);
// return coreGroup;
// }
// const objects = coreGroup.threejsObjectsWithGeo();
// for (let object of objects) {
// const mesh = object as Mesh;
// if (mesh.isMesh) {
// this._unwrapMeshUVsWithAtlas(xatlas, mesh, params);
// }
// }
// return coreGroup;
// }
// private _unwrapMeshUVsWithAtlas(xatlas: XAtlasManager, mesh: Mesh, params: UvUnwrapSopParams) {
// const geometry = mesh.geometry;
// if (!geometry.index) {
// return;
// }
// const originalVertexCount = geometry.attributes.position.count;
// const originalIndexCount = geometry.index.count;
// try {
// xatlas.createAtlas();
// } catch (err) {
// this._node?.states.error.set('failed to create atlas');
// return;
// }
// const meshInfo = xatlas.createMesh(originalVertexCount, originalIndexCount, true, true);
// const index = geometry.getIndex();
// const positionAttrib = geometry.getAttribute(Attribute.POSITION);
// const normalAttrib = geometry.getAttribute(Attribute.NORMAL);
// const uvAttrib = geometry.getAttribute(Attribute.UV);
// if (!(index && positionAttrib && normalAttrib && uvAttrib)) {
// this.states?.error.set(`the geometry needs to have an index, position, normal and uv attributes`);
// return;
// }
// xatlas.HEAPU16.set(geometry.index.array, meshInfo.indexOffset / Uint16Array.BYTES_PER_ELEMENT);
// xatlas.HEAPF32.set(
// (geometry.attributes.position as BufferAttribute).array,
// meshInfo.positionOffset / Float32Array.BYTES_PER_ELEMENT
// );
// xatlas.HEAPF32.set(
// (geometry.attributes.normal as BufferAttribute).array,
// meshInfo.normalOffset / Float32Array.BYTES_PER_ELEMENT
// );
// xatlas.HEAPF32.set(
// (geometry.attributes.uv as BufferAttribute).array,
// meshInfo.uvOffset / Float32Array.BYTES_PER_ELEMENT
// );
// const statusCode = xatlas.addMesh();
// if (statusCode !== AddMeshStatus.Success) {
// throw new Error(`UVUnwrapper: Error adding mesh. Status code ${statusCode}`);
// }
// // const chartOptions: ChartOptions = {
// // fixWinding: true,
// // maxBoundaryLength: 0,
// // maxChartArea: 0,
// // maxCost: 2,
// // maxIterations: 1,
// // normalDeviationWeight: 2,
// // normalSeamWeight: 4,
// // roundnessWeight: 0.009999999776482582,
// // straightnessWeight: 6,
// // textureSeamWeight: 0.5,
// // useInputMeshUvs: false,
// // };
// // const packOptions: PackOptions = {
// // bilinear: true,
// // blockAlign: false,
// // bruteForce: false,
// // createImage: false,
// // maxChartSize: 0,
// // padding: 0,
// // resolution: 0,
// // rotateCharts: true,
// // rotateChartsToAxis: true,
// // texelsPerUnit: 0,
// // };
// // console.log({chartOptions, packOptions});
// try {
// xatlas.generateAtlas();
// } catch (err) {
// this._node?.states.error.set('failed to generate atlas');
// console.log(err);
// return;
// }
// const meshData = xatlas.getMeshData(meshInfo.meshId);
// const oldPositionArray = (geometry.attributes.position as BufferAttribute).array;
// const oldNormalArray = (geometry.attributes.normal as BufferAttribute).array;
// const oldUvArray = (geometry.attributes.uv as BufferAttribute).array;
// const newPositionArray = new Float32Array(meshData.newVertexCount * 3);
// const newNormalArray = new Float32Array(meshData.newVertexCount * 3);
// const newUvArray = new Float32Array(meshData.newVertexCount * 2);
// const newUv2Array = new Float32Array(xatlas.HEAPF32.buffer, meshData.uvOffset, meshData.newVertexCount * 2);
// const newIndexArray = new Uint32Array(xatlas.HEAPU32.buffer, meshData.indexOffset, meshData.newIndexCount);
// const originalIndexArray = new Uint32Array(
// xatlas.HEAPU32.buffer,
// meshData.originalIndexOffset,
// meshData.newVertexCount
// );
// for (let i = 0; i < meshData.newVertexCount; i++) {
// const originalIndex = originalIndexArray[i];
// // P
// newPositionArray[i * 3] = oldPositionArray[originalIndex * 3];
// newPositionArray[i * 3 + 1] = oldPositionArray[originalIndex * 3 + 1];
// newPositionArray[i * 3 + 2] = oldPositionArray[originalIndex * 3 + 2];
// // N
// newNormalArray[i * 3] = oldNormalArray[originalIndex * 3];
// newNormalArray[i * 3 + 1] = oldNormalArray[originalIndex * 3 + 1];
// newNormalArray[i * 3 + 2] = oldNormalArray[originalIndex * 3 + 2];
// // uv
// newUvArray[i * 2] = oldUvArray[originalIndex * 2];
// newUvArray[i * 2 + 1] = oldUvArray[originalIndex * 2 + 1];
// }
// // check inverted uvs (which face toward -z when set onto P)
// // const pointsCount = newPositionArray.length / 3;
// // const polyCount = newIndexArray.length / 3;
// // const maxI = polyCount * 3;
// // const uvLightmapFlipped: number[] = new Array(newPositionArray.length / 3).fill(-1);
// // for (let i = 0; i < maxI; i += 3) {
// // const i0 = newIndexArray[i];
// // const i1 = newIndexArray[i + 1];
// // const i2 = newIndexArray[i + 2];
// // _uvTriangle.a.set(newUv2Array[i0 * 2], newUv2Array[i0 * 2 + 1], 0);
// // _uvTriangle.b.set(newUv2Array[i1 * 2], newUv2Array[i1 * 2 + 1], 0);
// // _uvTriangle.c.set(newUv2Array[i2 * 2], newUv2Array[i2 * 2 + 1], 0);
// // _uvTriangle.getNormal(_uvTriangleN);
// // const flipped = _uvTriangleN.z < 0 ? 1 : 0;
// // // if (flipped) {
// // // // newIndexArray[i] = i2;
// // // // newIndexArray[i + 2] = i0;
// // // // newUv2Array[i0 * 2] = _uvTriangle.c.x;
// // // // newUv2Array[i0 * 2 + 1] = _uvTriangle.c.y;
// // // // newUv2Array[i2 * 2] = _uvTriangle.a.x;
// // // // newUv2Array[i2 * 2 + 1] = _uvTriangle.a.y;
// // // }
// // uvLightmapFlipped[i0] = flipped;
// // uvLightmapFlipped[i1] = flipped;
// // uvLightmapFlipped[i2] = flipped;
// // // if (_uvTriangleN.z < 0) {
// // // newNormalArray[i] *= -1;
// // // newNormalArray[i + 1] *= -1;
// // // newNormalArray[i + 2] *= -1;
// // // }
// // }
// // for (let i = 0; i < pointsCount; i++) {
// // const flipped = uvFlip[i];
// // console.log(i, flipped);
// // newNormalArray[i * 3] *= flipped;
// // newNormalArray[i * 3 + 1] *= flipped;
// // newNormalArray[i * 3 + 2] *= flipped;
// // }
// // create geo
// const newGeometry = new BufferGeometry();
// newGeometry.setAttribute('position', new Float32BufferAttribute(newPositionArray, 3));
// newGeometry.setAttribute('normal', new Float32BufferAttribute(newNormalArray, 3));
// if (params.uv != Attribute.UV) {
// newGeometry.setAttribute('uv', new Float32BufferAttribute(newUvArray, 2));
// }
// newGeometry.setAttribute(params.uv, new Float32BufferAttribute(newUv2Array, 2));
// // newGeometry.setAttribute(UV_LIGHT_MAP_FLIPPED_ATTRIB_NAME, new Float32BufferAttribute(uvLightmapFlipped, 1));
// newGeometry.setIndex(new Uint32BufferAttribute(newIndexArray, 1));
// mesh.geometry = newGeometry;
// xatlas.destroyAtlas();
// }
private _unwrapMeshUVsWithPotpack(inputCoreGroups: CoreGroup[], params: UvUnwrapSopParams) {
const coreGroup = inputCoreGroups[0];
const objects = coreGroup.threejsObjectsWithGeo();
for (let object of objects) {
const mesh = object as Mesh;
if (mesh.isMesh) {
this._unwrapUVsWithPotpack(mesh, params);
}
}
return coreGroup;
}
// TODO: at the moment each polygon will fix a single box
// when ideally this should find when 2 triangles form a quad or square
// and could then fit in the box
private _unwrapUVsWithPotpack(mesh: Mesh, params: UvUnwrapSopParams) {
const geometry = mesh.geometry;
const indexArray = geometry.getIndex()?.array;
if (!indexArray) {
return;
}
const positionArray = (geometry.attributes.position as BufferAttribute)?.array;
if (!positionArray) {
return;
}
const uvArray = (geometry.attributes['uv'] as BufferAttribute)?.array;
if (!uvArray) {
return;
}
const polyCount = indexArray.length / 3;
const boxes: PotPackBox[] = new Array(polyCount);
for (let i = 0; i < polyCount; i++) {
// this take one edge (v1-v2) of the polygon and calculate its length (w)
// then we measure the distance between the mid point of that edge (vMid)
// and its projection again an edge parallel to the first edge (v1-v2), but going through v3.
v1.fromArray(positionArray, 3 * indexArray[3 * i + 0]);
v2.fromArray(positionArray, 3 * indexArray[3 * i + 1]);
v3.fromArray(positionArray, 3 * indexArray[3 * i + 2]);
let w = v1.distanceTo(v2);
vMid.copy(v1).add(v2).multiplyScalar(0.5);
line.start.copy(v3);
line.end.copy(v3).add(v2).sub(v1);
line.closestPointToPoint(vMid, false, vEnd);
let h = vMid.distanceTo(vEnd);
// we try and get some order to that by
// always having h and sorted
if (h < w) {
const tmp = h;
h = w;
w = tmp;
}
boxes[i] = {w, h};
}
const result = Potpack(boxes);
const newUvValues = new Array(uvArray.length);
// function setnewValue(index: number, newValue: number) {
// // if (newUvValues[index] == null) {
// newUvValues[index] = newValue;
// // } else {
// // if (newUvValues[index] <= newValue) {
// // newUvValues[index] = newValue;
// // } else {
// // console.log(`${index} already has ${newUvValues[index]}, cannot be set to ${newValue}`);
// // }
// // }
// }
for (let i = 0; i < polyCount; i++) {
const box = boxes[i] as PotPackBoxResult;
const x = box.x / result.w;
const y = box.y / result.h;
const w = box.w / result.w;
const h = box.h / result.h;
const index0 = 2 * indexArray[i * 3 + 0];
const index1 = 2 * indexArray[i * 3 + 1];
const index2 = 2 * indexArray[i * 3 + 2];
newUvValues[index0] = x;
newUvValues[index0 + 1] = y;
newUvValues[index1] = x + w;
newUvValues[index1 + 1] = y;
newUvValues[index2] = x;
newUvValues[index2 + 1] = y + h;
}
geometry.setAttribute(params.uv, new Float32BufferAttribute(newUvValues, 2));
}
}