Purely experimental and not necessarily accurate.
Using xatlas-web
and xatlas-three
in separate viewers, just to ease the comparison of the outcome provided by each viewer.
EDIT: xatlas-web
seems to work in most if not all browsers while xatlas-three
might only work properly in Firefox browser (other browsers should throw cross-origin error).
UVs should only get created if at least 1 texture is loaded together with the model. The viewers T
button will allow switching the texture. Any complex model might take some time to create UVs.
Probably the easiest test could be done with some STL model and some random textures from three.js repository - maybe try slotted_disk.stl
model for simplicity.
Check the viewers code and search for uvUnwrapper
to see all applicable usage and read any comments. The following attachment might have bugs and should be used to only examine the procedure of using these libraries but for the actual testing see the links provided afterwards.
PLY Viewer - Standalone (r170).zip (35.1 KB)
Consider using either the WebGPU Standalone version or the online PLY+STL Viewer . Both include lots of bug fixes and should also create UVs for mesh models with vertex colors (a popup should warn about removing vertex colors in order to proceed). These viewers should work in most browsers.
Here are pictures showing the WebGPU Standalone viewer with Dolphins PLY and Slotted Disk STL models with applied UV texture (all example files from three.js repository)
2 Likes
geyang
December 29, 2024, 10:53pm
2
I tested it out, the T button is disabled.
The WebGPU Standalone version should be functional and with the least bugs so try that one.
It does not require WebGPU since it would fall back to using WebGL2 instead.
Cool stuff. Is there a way to wrap this in some kind of THREE.UnwrapModifier ? or do one of your referenced libraries provide such an api?
I’d love to have a nice easy unwrapper for regular webGL2…
I am not sure how accurate this all is and nobody should have any high expectations from it.
You should check the code of that WebGPU Standalone file, which kind of narrows down to this:
// Importmap
<script type="importmap">
{
"imports": {
"xatlas-web": "https://esm.sh/xatlas-web@0.1.0"
}
}
</script>
// 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
};
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;
}
unwrapGeometry( 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();
}
}
An instance of this class should be created when one wants to unwrap the geometry, which can be seen in the full code.
I found xatlas-web
easier to use for my purposes but am not sure if xatlas-three
might actually be a better choice for you.
Just try to do a bit of a research on both.
2 Likes
Nice.
Don’t hesitate to explain to users what you did on your end to achieve this (if it is any different than previous explanations).
1 Like
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();
3 Likes