Save this as three-unwrap.js
import *as XAtlas from "https://esm.sh/xatlas-web@0.1.0"
import *as THREE from "three"
// Modified version of UVUnwrapper: https://github.com/gkjohnson/three-gpu-pathtracer/blob/6e5a452bb1ecb2853dbaa6d5a57682b814df504c/src/utils/UVUnwrapper.js
// Using xatlas-web: https://github.com/mozillareality/xatlas-web
const AddMeshStatus = {
Success: 0,
Error: 1,
IndexOutOfRange: 2,
InvalidIndexCount: 3
};
export class UVUnwrapper {
constructor() {
this._module = null;
}
async load() {
const wasmurl = new URL('https://cdn.jsdelivr.net/npm/xatlas-web@0.1.0/dist/xatlas-web.wasm',import.meta.url);
this._module = XAtlas['default']({
locateFile(path) {
if (path.endsWith('.wasm')) {
return wasmurl.toString();
}
return path;
}
});
return this._module.ready;
}
generate(geometry) {
const xatlas = this._module;
const originalVertexCount = geometry.attributes.position.count;
const originalIndexCount = geometry.index.count;
xatlas.createAtlas();
const meshInfo = xatlas.createMesh(originalVertexCount, originalIndexCount, true, true);
xatlas.HEAPU16.set(geometry.index.array, meshInfo.indexOffset / Uint16Array.BYTES_PER_ELEMENT);
xatlas.HEAPF32.set(geometry.attributes.position.array, meshInfo.positionOffset / Float32Array.BYTES_PER_ELEMENT);
xatlas.HEAPF32.set(geometry.attributes.normal.array, meshInfo.normalOffset / Float32Array.BYTES_PER_ELEMENT);
const statusCode = xatlas.addMesh();
if (statusCode !== AddMeshStatus.Success) {
throw new Error(`UVUnwrapper: Error adding mesh. Status code ${statusCode}`);
}
xatlas.generateAtlas();
const meshData = xatlas.getMeshData(meshInfo.meshId);
const oldPositionArray = geometry.attributes.position.array;
const oldNormalArray = geometry.attributes.normal.array;
const newPositionArray = new Float32Array(meshData.newVertexCount * 3);
const newNormalArray = new Float32Array(meshData.newVertexCount * 3);
const newUvArray = 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];
newPositionArray[i * 3] = oldPositionArray[originalIndex * 3];
newPositionArray[i * 3 + 1] = oldPositionArray[originalIndex * 3 + 1];
newPositionArray[i * 3 + 2] = oldPositionArray[originalIndex * 3 + 2];
newNormalArray[i * 3] = oldNormalArray[originalIndex * 3];
newNormalArray[i * 3 + 1] = oldNormalArray[originalIndex * 3 + 1];
newNormalArray[i * 3 + 2] = oldNormalArray[originalIndex * 3 + 2];
}
geometry.deleteAttribute('position');
geometry.deleteAttribute('normal');
geometry.setAttribute('position', new THREE.Float32BufferAttribute(newPositionArray,3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(newNormalArray,3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(newUvArray,2));
geometry.setIndex(new THREE.Uint32BufferAttribute(newIndexArray,1));
xatlas.destroyAtlas();
}
}
To test it on a sphere:
import {UVUnwrapper} from "./three-unwrap.js"
let unwrapper = new UVUnwrapper();
await unwrapper.load();
let sphere = new THREE.SphereGeometry();
unwrapper.generate(sphere);
scene.add(new THREE.Mesh(sphere,new THREE.MeshBasicMaterial({wireframe:true,color:"#101020"})))
To visualize the UV map:
let generateUVMesh = ()=>{
let uvGeom = sphere.clone();
let v = uvGeom.attributes.position
let uv = uvGeom.attributes.uv
for(let i=0;i<v.count;i++){
let x = uv.getX(i)
let y = uv.getY(i)
v.setX(i,x)
v.setY(i,y)
v.setZ(i,0);
}
let m = new THREE.Mesh(uvGeom,new THREE.MeshBasicMaterial({wireframe:true,color:"#051005"}))
m.position.y = 10;
m.scale.multiplyScalar(10)
scene.add(m)
}
generateUVMesh();