UV Unwrapping with XAtlas #792
Replies: 2 comments 4 replies
-
Hi @lucas-jones, this looks awesome! At first reading (I don't know much about XAtlas!), I don't see any problems with the proof of concept. I could imagine this being really helpful for (a) optimizing scenes to use fewer meshes and textures, and (b) generating UVs to use in lightmapping or AO baking with tools like three-gpu-pathtracer. It does assume the input glTF file meets some preconditions: having float32 position, normal, and uv attributes, and no other vertex attributes. Plenty of glTF files meet those requirements, or could be modified to do so. No pressure if you want to leave it at that. But I am definitely curious if the more general case — with an unknown number of other vertex attributes — could also be supported? Based on this discussion, it sounds like xatlas has Related discussions: |
Beta Was this translation helpful? Give feedback.
-
Here is a more complete impl based on xatlas-wasi, which is a thin wrapper over the c api of upstream xatlas: In this code, code
import type {
Accessor,
Document,
GLTF,
Material,
Node,
Primitive,
Property,
Scene,
Transform,
TypedArray,
} from '@gltf-transform/core';
import {AnimationChannel, Root,} from '@gltf-transform/core';
import {assignDefaults, createTransform,} from "@gltf-transform/functions";
import type {WasmExports,} from "xatlas-wasi";
import {
WasmContext,
xatlasAtlas,
xatlasChartOptions,
xatlasMesh,
xatlasMeshDecl,
xatlasPackOptions,
} from "xatlas-wasi";
const NAME = 'xatlas';
export interface XatlasChartOptions extends Omit<xatlasChartOptions, 'SIZE' | 'read' | 'write' | 'paramFunc'> {
}
export interface XatlasPackOptions extends Omit<xatlasPackOptions, 'SIZE' | 'read' | 'write'> {
}
/**
* Enum representing different grouping functions for xatlas.
*/
export const enum XatlasGroupingFunction {
/** Group every primitive for a single mesh. */
SingleMesh,
/** Group by material and attribute types. */
MaterialAttribute,
/** Group by parent node and attribute types. */
ParentAttribute,
/** Group by material, parent node, and attribute types. */
MaterialParentAttribute
}
/**
* Enum representing the coordinate space used by xatlas.
*/
export const enum XatlasSpace {
/** Local coordinate space. */
Local
}
/**
* Enum representing the vertex order options for xatlas.
* @see https://github.com/jpcy/xatlas/issues/95#issuecomment-1015673872
*/
export const enum XatlasVertexOrder {
/**
* Use the vertex order and indices generated by xatlas.
* Vertices could be much bigger and harder to optimize.
* Recommended to optimize the model and then use this.
*/
XatlasVertexOrder,
/**
* Use vertex order of the original mesh.
* This results in a much smaller size, but might have UV mismatches
* if vertices are split by xatlas.
* In this case, the last UV from xatlas is used.
*/
OriginalVertexOrderLastUv,
/**
* Same as `OriginalVertexOrderLastUv`, except that it uses the first UV instead of the last one.
* Slightly slower than the above option since it has branches,
* and some UVs could be missing from the final result.
*/
OriginalVertexOrderFirstUv,
}
/**
* Options for the {@link xatlas} function.
*/
export interface XatlasOptions {
/**
* xatlas WebAssembly exports or a promise resolving to them.
*/
xatlas?: WasmExports | Promise<WasmExports>;
/**
* Options for chart generation in xatlas.
* Use `null` to disable chart generation.
*/
chartOptions?: Partial<XatlasChartOptions> | null;
/**
* Options for packing in xatlas.
* Use `null` to disable packing.
*/
packOptions?: Partial<XatlasPackOptions> | null;
/**
* Attribute name to use.
* Use "AUTO" to generate a name when possible.
* Use "TEXCOORD_0", "TEXCOORD_1", etc., to use a fixed attribute name.
*/
attributeName?: string;
/**
* Maximum `TEXCOORD_{n}` to generate.
*/
maxAutoAttribute?: number;
/**
* Whether to overwrite if the primitive has the attribute.
*/
overwrite?: boolean;
/**
* Function or enum to iterate, filter, and group primitives for UV generation using xatlas.
*/
groupingFunction?: XatlasGroupingFunction | ((document: Document, scene: Scene) => Primitive[][]);
/**
* Coordinate space to use.
* TODO: Implement UV generation in world space.
* TODO: Accept a matrix * vector function here for world space transform.
*/
space?: XatlasSpace;
/**
* Vertex order to use.
*/
vertexOrder?: XatlasVertexOrder;
}
/// region grouping functions
function singleMeshGrouping(document: Document, scene: Scene): Primitive[][] {
const set = new Set<Primitive>();
scene.traverse(node => {
const mesh = node.getMesh();
if (mesh) {
for (const primitive of mesh.listPrimitives()) {
set.add(primitive);
}
}
});
const groups: Primitive[][] = [];
for (const primitive of set) {
groups.push([primitive]);
}
return groups;
}
function attributeGroupingKey(primitive: Primitive): string {
let flag = 0;
if (primitive.getIndices()?.getArray()?.length) {
flag |= 1;
}
for (const semantics of primitive.listSemantics()) {
if (semantics === 'POSITION') {
flag |= 2;
} else if (semantics === 'NORMAL') {
flag |= 4;
} else if (semantics === 'TEXCOORD_0') {
flag |= 8;
}
}
return flag.toString(16);
}
function materialAttributeGrouping(document: Document, scene: Scene) {
const map = new Map<Material, Record<string, Primitive[]>>();
const noMaterialMap: Record<string, Primitive[]> = {};
scene.traverse(node => {
const mesh = node.getMesh();
if (mesh) {
for (const primitive of mesh.listPrimitives()) {
if (!primitive.getAttribute('POSITION')) {
continue;
}
const mat = primitive.getMaterial();
if (mat && !map.has(mat)) {
map.set(mat, {});
}
const map2 = mat ? map.get(mat) : noMaterialMap;
const key = attributeGroupingKey(primitive);
if (!map2[key]) {
map2[key] = [];
}
map2[key].push(primitive);
}
}
});
const groups = [];
for (const k in noMaterialMap) {
const v = noMaterialMap[k];
if (v && v.length) {
groups.push(v);
}
}
for (const value of map.values()) {
for (const k in value) {
const v = value[k];
if (v && v.length) {
groups.push(v);
}
}
}
return groups;
}
function parentAttributeGrouping(document: Document, scene: Scene) {
const map = new Map<Scene | Node, Record<string, Primitive[]>>();
scene.traverse(node => {
const mesh = node.getMesh();
if (mesh) {
for (const primitive of mesh.listPrimitives()) {
if (!primitive.getAttribute('POSITION')) {
continue;
}
const parent = node.getParentNode() || scene;
if (!map.has(parent)) {
map.set(parent, {});
}
const map2 = map.get(parent);
const key = attributeGroupingKey(primitive);
if (!map2[key]) {
map2[key] = [];
}
map2[key].push(primitive);
}
}
});
const groups = [];
for (const value of map.values()) {
for (const k in value) {
const v = value[k];
if (v && v.length) {
groups.push(v);
}
}
}
return groups;
}
function materialParentAttributeGrouping(document: Document, scene: Scene) {
const map =
new Map<Material, Map<Node | Scene, Record<string, Primitive[]>>>();
const noMaterialMap =
new Map<Node | Scene, Record<string, Primitive[]>>();
scene.traverse(node => {
const mesh = node.getMesh();
if (mesh) {
for (const primitive of mesh.listPrimitives()) {
if (!primitive.getAttribute('POSITION')) {
continue;
}
const mat = primitive.getMaterial();
if (mat && !map.has(mat)) {
map.set(mat, new Map());
}
const map2 = mat ? map.get(mat) : noMaterialMap;
const parent = node.getParentNode() || scene;
if (!map2.has(parent)) {
map2.set(parent, {});
}
const map3 = map2.get(parent);
const key = attributeGroupingKey(primitive);
if (!map3[key]) {
map3[key] = [];
}
map3[key].push(primitive);
}
}
});
const groups = [];
for (const map3 of noMaterialMap.values()) {
for (const k in noMaterialMap) {
const v = map3[k];
if (v && v.length) {
groups.push(v);
}
}
}
for (const k in noMaterialMap) {
const map3 = noMaterialMap[k];
for (const value of map3.values()) {
for (const k in value) {
const v = value[k];
if (v && v.length) {
groups.push(v);
}
}
}
}
for (const map2 of map.values()) {
for (const map3 of map2.values()) {
for (const k in noMaterialMap) {
const v = map3[k];
if (v && v.length) {
groups.push(v);
}
}
}
}
return groups;
}
const groupingFunctionMap: Record<XatlasGroupingFunction, (document: Document, scene: Scene) => Primitive[][]> = {
[XatlasGroupingFunction.SingleMesh]: singleMeshGrouping,
[XatlasGroupingFunction.MaterialAttribute]: materialAttributeGrouping,
[XatlasGroupingFunction.ParentAttribute]: parentAttributeGrouping,
[XatlasGroupingFunction.MaterialParentAttribute]: materialParentAttributeGrouping,
};
/// endregion grouping functions
export const XATLAS_DEFAULTS: Omit<XatlasInternalOptions, 'xatlas'> = {
chartOptions: null,
packOptions: null,
attributeName: 'AUTO',
maxAutoAttribute: 6,
overwrite: false,
groupingFunction: singleMeshGrouping,
space: XatlasSpace.Local,
vertexOrder: XatlasVertexOrder.XatlasVertexOrder,
optionsPtr: 0,
chartOptionsPtr: 0,
packOptionsPtr: 0,
};
interface XatlasInternalOptions extends Required<XatlasOptions> {
// internal ptr for reusing options
optionsPtr: number;
chartOptionsPtr: number;
packOptionsPtr: number;
}
export function xatlas(_options: XatlasOptions = XATLAS_DEFAULTS): Transform {
const options =
assignDefaults(XATLAS_DEFAULTS, _options) as XatlasInternalOptions;
const xatlas = options.xatlas;
if (!xatlas) {
throw new Error(`${NAME}: xatlas dependency required.`);
}
const groupingFunction = typeof options.groupingFunction === 'function' ?
options.groupingFunction :
groupingFunctionMap[options.groupingFunction];
if (!groupingFunction) {
throw new Error(`${NAME}: invalid groupingFunction ${options.groupingFunction}.`);
}
return createTransform(NAME, (document: Document) => Promise.resolve(xatlas).then(xatlas => {
const ctx = new WasmContext(xatlas);
const root = document.getRoot();
const logger = document.getLogger();
// reuse options for all matching meshes
xatlasInitOptions(options, xatlas, ctx);
// xatlas
for (const scene of root.listScenes()) {
const groups = groupingFunction(document, scene);
for (const group of groups) {
xatlasPrimitives(document, group, ctx, xatlas, options);
}
}
// free reused options
if (options.optionsPtr) {
xatlas.free(options.optionsPtr);
}
options.optionsPtr = 0;
options.chartOptionsPtr = 0;
options.packOptionsPtr = 0;
// cleanup not seems needed, since we manually pruned it loop
logger.debug(`${NAME}: Complete.`);
}));
}
function xatlasInitOptions(
options: XatlasInternalOptions,
xa: WasmExports,
ctx: WasmContext,
): void {
const hasChartOptions = options.chartOptions && !isEmptyObject(options.chartOptions);
const hasPackOptions = options.packOptions && !isEmptyObject(options.packOptions);
let optionsSize = 0;
if (hasChartOptions) {
optionsSize += xatlasChartOptions.SIZE;
}
if (hasPackOptions) {
optionsSize += xatlasPackOptions.SIZE;
}
let allocatePtr = 0;
let chartOptionsPtr = 0;
if (optionsSize) {
allocatePtr = xa.malloc(optionsSize);
ctx.reload();
}
if (hasChartOptions) {
chartOptionsPtr = allocatePtr;
const chartOptions = new xatlasChartOptions();
xa.xatlasChartOptionsInit(chartOptionsPtr);
chartOptions.read(chartOptionsPtr, ctx);
for (let chartOptionsKey in options.chartOptions) {
if (chartOptionsKey in chartOptions &&
options.chartOptions[chartOptionsKey] !== undefined) {
chartOptions[chartOptionsKey] = options.chartOptions[chartOptionsKey];
}
}
chartOptions.write(chartOptionsPtr, ctx);
}
let packOptionsPtr = 0;
if (hasPackOptions) {
packOptionsPtr = hasChartOptions ? chartOptionsPtr + xatlasChartOptions.SIZE : allocatePtr;
const packOptions = new xatlasPackOptions();
xa.xatlasPackOptionsInit(chartOptionsPtr);
packOptions.read(packOptionsPtr, ctx);
for (let packOptionsKey in options.packOptions) {
if (packOptionsKey in packOptions
&& options.packOptions[packOptionsKey] !== undefined) {
packOptions[packOptionsKey] = options.packOptions[packOptionsKey];
}
}
packOptions.write(packOptionsPtr, ctx);
}
options.optionsPtr = allocatePtr;
options.chartOptionsPtr = chartOptionsPtr;
options.packOptionsPtr = packOptionsPtr;
}
function remapAttributes(
document: Document,
options: XatlasInternalOptions,
ctx: WasmContext,
primitive: Primitive,
meshInfo: xatlasMesh,
originalIndicesPtr: number
): Float32Array {
const uvPtr = originalIndicesPtr + meshInfo.vertexCount * 4;
// just read as view, would copy buffers later if needed
const newIndexData = ctx.readU32(meshInfo.indexArray, meshInfo.indexCount);
const originalIndexData = ctx.readU32(originalIndicesPtr, meshInfo.vertexCount);
let uvData = ctx.readF32(uvPtr, meshInfo.vertexCount * 2);
if (options.vertexOrder === XatlasVertexOrder.XatlasVertexOrder) {
for (const oldAttribute of deepListAttributes(primitive)) {
// shallow clone original and swap
const attribute = shallowCloneAccessor(document, oldAttribute);
const elementSize = attribute.getElementSize();
const oldArray = oldAttribute.getArray();
const newArray = new Float32Array(meshInfo.vertexCount * elementSize);
for (let i = 0, l = meshInfo.vertexCount; i < l; i++) {
let originalIndex = originalIndexData[i];
for (let index = 0; index < elementSize; index++) {
newArray[elementSize * i + index] = oldArray[elementSize * originalIndex + index];
}
}
attribute.setArray(newArray);
deepSwapAttribute(primitive, oldAttribute, attribute);
// dispose original attribute if not used
prune(oldAttribute);
}
const oldIndices = primitive.getIndices();
const indices = document.createAccessor();
primitive.setIndices(indices);
indices.setArray(meshInfo.vertexCount >= 65534 ?
newIndexData.slice() :
new Uint16Array(newIndexData));
// dispose original indices if not used
if (oldIndices) {
prune(oldIndices);
}
// clone it to js memory
return uvData.slice();
} else if (options.vertexOrder === XatlasVertexOrder.OriginalVertexOrderLastUv) {
const remappedUv =
new Float32Array(primitive.listAttributes()[0].getCount() * 2);
// https://github.com/jpcy/xatlas/issues/95#issuecomment-1015673872
for (let i = 0, l = meshInfo.vertexCount; i < l; i++) {
let originalIndex = originalIndexData[i];
remappedUv[originalIndex * 2] = uvData[i * 2];
remappedUv[originalIndex * 2 + 1] = uvData[i * 2 + 1];
}
return remappedUv;
} else {
const vertexCount = primitive.listAttributes()[0].getCount();
const bitset = new BitSet(vertexCount);
const remappedUv =
new Float32Array(primitive.listAttributes()[0].getCount() * 2);
// https://github.com/jpcy/xatlas/issues/95#issuecomment-1015673872
for (let i = 0, l = meshInfo.vertexCount; i < l; i++) {
let originalIndex = originalIndexData[i];
if (!bitset.get(originalIndex)) {
bitset.set(originalIndex);
remappedUv[originalIndex * 2] = uvData[i * 2];
remappedUv[originalIndex * 2 + 1] = uvData[i * 2 + 1];
}
}
return remappedUv;
}
}
// TODO: accept a transform matrix here, and accept positions in world space
function xatlasPrimitives(
document: Document, primitives: Primitive[],
ctx: WasmContext, xa: WasmExports,
options: XatlasInternalOptions
): void {
const logger = document.getLogger();
const map: Map<Primitive, number> = new Map();
const added: Primitive[] = [];
const atlas = xa.xatlasCreate();
let meshId = 0;
for (let index = 0; index < primitives.length; index++) {
const primitive = primitives[index];
if (map.has(primitive)) continue;
// TRIANGLES
if (primitive.getMode() !== 4) {
logger.warn(`${NAME}: skipped non-TRIANGLES primitive`);
// TODO: convert others
continue;
}
if (!options.overwrite && primitive.getAttribute(options.attributeName)) {
let attributeName = options.attributeName;
if (attributeName === 'AUTO') {
for (let i = 0; i <= options.maxAutoAttribute; i++) {
attributeName = 'TEXCOORD_' + i;
if (!primitive.getAttribute(attributeName)) {
break;
}
}
}
if (primitive.getAttribute(attributeName)) {
logger.debug(`${NAME}: skipped a primitive to avoid overwrite`);
continue;
}
}
let size = xatlasMeshDecl.SIZE;
let indices = primitive.getIndices();
const positions = primitive.getAttribute('POSITION');
const normals = primitive.getAttribute('NORMAL');
// only one of uvs supported
const uvs = primitive.getAttribute('TEXCOORD_0');
const positionsArr = getFloatArray(positions);
const normalsArr = getFloatArray(normals);
const uvsArr = getFloatArray(uvs);
if (indices) {
// raw size, since both u16 and u32 supported
size += indices.getByteLength();
}
// only f32 supported
if (positions && positionsArr) {
size += positionsArr.length * 4;
} else {
logger.warn(`${NAME}: skipped primitive without position`);
continue;
}
if (normals && normalsArr) {
size += normalsArr.length * 4;
}
if (uvs && uvsArr) {
size += uvsArr.length * 4;
}
const meshDeclPtr = xa.malloc(size);
xa.xatlasMeshDeclInit(meshDeclPtr);
ctx.reload();
let meshObj = new xatlasMeshDecl();
meshObj.read(meshDeclPtr, ctx);
let ptr = meshDeclPtr + xatlasMeshDecl.SIZE;
// set f32 attrs first, to avoid alignment issues
if (positions && positionsArr) {
ctx.writeF32(ptr, positionsArr);
meshObj.vertexPositionData = ptr;
meshObj.vertexPositionStride = 12;
meshObj.vertexCount = positions.getCount();
ptr += positionsArr.length * 4;
}
if (normals && normalsArr) {
ctx.writeF32(ptr, normalsArr);
meshObj.vertexNormalData = ptr;
meshObj.vertexNormalStride = 12;
ptr += normalsArr.length * 4;
}
if (uvs && uvsArr) {
ctx.writeF32(ptr, uvsArr);
meshObj.vertexUvData = ptr;
meshObj.vertexUvStride = 8;
ptr += uvsArr.length * 4;
}
if (indices) {
const indicesArray = indices.getArray();
if (indicesArray instanceof Uint32Array) {
meshObj.indexCount = indicesArray.length;
meshObj.indexData = ptr;
meshObj.indexFormat = 1;
ctx.writeU32(ptr, indicesArray);
} else if (indicesArray instanceof Uint16Array) {
meshObj.indexCount = indicesArray.length;
meshObj.indexData = ptr;
meshObj.indexFormat = 0;
ctx.writeU16(ptr, indicesArray);
} else {
logger.warn(`${NAME}: skipped invalid non-u32 and non-u16 indices`);
}
}
meshObj.write(meshDeclPtr, ctx);
const error = xa.xatlasAddMesh(atlas, meshDeclPtr, 1);
if (error) {
// TODO: readable error with xatlasAddMeshErrorString
logger.error(`${NAME}: xatlasAddMesh error ${error}`);
} else {
added[meshId] = primitive;
map.set(primitive, meshId++);
}
xa.free(meshDeclPtr);
}
// skip this if nothing added successfully
if (!added.length) {
logger.warn(`${NAME}: no mesh in group, skipping.`);
xa.xatlasDestroy(atlas);
ctx.reload();
return;
}
// make the debug logging
if (xa.xatlasAddMeshJoin) {
xa.xatlasAddMeshJoin(atlas);
}
xa.xatlasGenerate(atlas, options.chartOptionsPtr, options.packOptionsPtr);
ctx.reload();
const atlasObj = new xatlasAtlas();
atlasObj.read(atlas, ctx);
if (atlasObj.meshCount !== added.length) {
logger.error(`${NAME}: mesh count mismatch, expected ${
added.length
}, actual ${atlasObj.meshCount}`);
xa.xatlasDestroy(atlas);
ctx.reload();
return;
}
const meshInfo = new xatlasMesh();
for (let i = 0; i < atlasObj.meshCount; i++) {
const meshPtr = atlasObj.meshes + (i * xatlasMesh.SIZE);
meshInfo.read(meshPtr, ctx);
const originalIndicesPtr = xa.malloc(meshInfo.vertexCount * 12);
xa.copy_uv(atlas, meshPtr, originalIndicesPtr);
ctx.reload();
const primitive = added[i];
const uvData = remapAttributes(
document, options, ctx, primitive, meshInfo, originalIndicesPtr);
// overwrite check is done before this
let attributeName = options.attributeName;
if (attributeName === 'AUTO') {
for (let i = 0; i <= options.maxAutoAttribute; i++) {
attributeName = 'TEXCOORD_' + i;
if (!primitive.getAttribute(attributeName)) {
break;
}
}
}
const oldAttribute = primitive.getAttribute(attributeName);
const newUv = document.createAccessor(attributeName)
.setType("VEC2")
.setArray(uvData);
xa.free(originalIndicesPtr);
primitive.setAttribute(attributeName, newUv);
if (oldAttribute) {
prune(oldAttribute);
}
}
xa.xatlasDestroy(atlas);
}
/// region utils
class BitSet {
declare private readonly size: number;
declare private readonly bits: Uint32Array;
constructor(size: number) {
this.size = size;
this.bits = new Uint32Array((size + 31) >>> 5); // Equivalent to Math.ceil(size / 32)
}
set(index: number): void {
if (index >= this.size) {
throw new RangeError("Index out of bounds");
}
const arrIndex = index >>> 5; // Equivalent to Math.floor(index / 32)
const bitPosition = index & 31; // Equivalent to index % 32
this.bits[arrIndex] |= (1 << bitPosition);
}
get(index: number): boolean {
if (index >= this.size) {
throw new RangeError("Index out of bounds");
}
const arrIndex = index >>> 5;
const bitPosition = index & 31;
return (this.bits[arrIndex] & (1 << bitPosition)) !== 0;
}
}
function dequantizeAttributeArray(
srcArray: ArrayLike<number>,
componentType: GLTF.AccessorComponentType,
normalized: boolean
) {
const dstArray = new Float32Array(srcArray);
if (normalized) {
// TODO: faster denormalize using wasm
switch (componentType) {
case 5126: // FLOAT
break;
case 5123: // UNSIGNED_SHORT
for (let j = 0, length = dstArray.length, i; j < length; j++) {
i = dstArray[j];
dstArray[j] = i / 65535.0;
}
break;
case 5121: // UNSIGNED_BYTE
for (let j = 0, length = dstArray.length, i; j < length; j++) {
i = dstArray[j];
dstArray[j] = i / 255.0;
}
break;
case 5122: // SHORT
for (let j = 0, length = dstArray.length, i; j < length; j++) {
i = dstArray[j];
dstArray[j] = Math.max(i / 32767.0, -1.0);
}
break;
case 5120: // BYTE
for (let j = 0, length = dstArray.length, i; j < length; j++) {
i = dstArray[j];
dstArray[j] = Math.max(i / 127.0, -1.0);
}
break;
default:
throw new Error('Invalid component type.');
}
}
return dstArray;
}
function getFloatArray(accessor?: Accessor): TypedArray | void {
if (!accessor) {
return;
}
const arr = accessor.getArray();
if (!arr || !arr.length) {
return;
}
if (accessor.getNormalized()) {
return dequantizeAttributeArray(arr, accessor.getComponentType(), true);
}
return arr;
}
function prune(prop: Property): void {
// Consider a property unused if it has no references from another property, excluding
// types Root and AnimationChannel.
const parents = prop.listParents().filter((p) =>
!(p instanceof Root || p instanceof AnimationChannel));
const needsExtras = !isEmptyObject(prop.getExtras());
if (!parents.length && !needsExtras) {
prop.dispose();
}
}
/** Clones an {@link Accessor} without creating a copy of its underlying TypedArray data. */
function shallowCloneAccessor(document: Document, accessor: Accessor): Accessor {
return document
.createAccessor(accessor.getName())
.setArray(accessor.getArray())
.setType(accessor.getType())
.setBuffer(accessor.getBuffer())
.setNormalized(accessor.getNormalized())
.setSparse(accessor.getSparse());
}
function isEmptyObject(object: Record<string, unknown>): boolean {
for (const key in object) return false;
return true;
}
/**
* Returns a list of all unique vertex attributes on the given primitive and
* its morph targets.
*/
function deepListAttributes(prim: Primitive): Accessor[] {
const accessors: Accessor[] = [];
for (const attribute of prim.listAttributes()) {
accessors.push(attribute);
}
for (const target of prim.listTargets()) {
for (const attribute of target.listAttributes()) {
accessors.push(attribute);
}
}
return Array.from(new Set(accessors));
}
function deepSwapAttribute(prim: Primitive, src: Accessor, dst: Accessor): void {
prim.swap(src, dst);
for (const target of prim.listTargets()) {
target.swap(src, dst);
}
}
/// endregion utils |
Beta Was this translation helpful? Give feedback.
-
Created a proof of concept of using XAtlas + glTF-Transform.
Very basic example, but might help someone in the future.
You can pass it an array of
Primatives
and it will generate a shared texture atlas - useful for light/ao maps - maybe even baking multiple textures into one.https://github.com/lucas-jones/gltf-transform-xatlas
First time using glTF-Transform so if you have any tips on how I could improve let us know!
Beta Was this translation helpful? Give feedback.
All reactions