273 lines
7.2 KiB
TypeScript
273 lines
7.2 KiB
TypeScript
import {
|
|
BoxGeometry,
|
|
BufferAttribute,
|
|
PlaneGeometry,
|
|
Vector2,
|
|
Vector3,
|
|
Vector4,
|
|
} from 'three';
|
|
import { MsonFace, MsonTexture } from '../mson';
|
|
import { convertRange } from '../util/num-range';
|
|
|
|
export class UVMapper {
|
|
// px nx py ny pz nz
|
|
static boxFaceOrder = ['west', 'east', 'up', 'down', 'north', 'south'];
|
|
static boxWidthSide: Record<string, 'x' | 'y' | 'z'> = {
|
|
east: 'z',
|
|
west: 'z',
|
|
up: 'x',
|
|
down: 'x',
|
|
south: 'x',
|
|
north: 'x',
|
|
};
|
|
|
|
static boxHeightSide: Record<string, 'x' | 'y' | 'z'> = {
|
|
east: 'y',
|
|
west: 'y',
|
|
up: 'z',
|
|
down: 'z',
|
|
south: 'y',
|
|
north: 'y',
|
|
};
|
|
|
|
static planeWidthSide: Record<string, 'x' | 'y'> = {
|
|
east: 'x',
|
|
west: 'x',
|
|
up: 'x',
|
|
down: 'x',
|
|
south: 'x',
|
|
north: 'x',
|
|
};
|
|
|
|
static planeHeightSide: Record<string, 'x' | 'y'> = {
|
|
east: 'y',
|
|
west: 'y',
|
|
up: 'y',
|
|
down: 'y',
|
|
south: 'y',
|
|
north: 'y',
|
|
};
|
|
|
|
/**
|
|
* Convert coordinates relative to the texture to UVs.
|
|
* @param sizeU Size of quad U
|
|
* @param sizeV Size of quad V
|
|
* @param txU Texture width
|
|
* @param txV Texture height
|
|
* @param setU Specified U offset
|
|
* @param setV Specified V offset
|
|
* @param invertU Invert U axis
|
|
* @param invertV Invert V axis
|
|
*/
|
|
static uvOffsetConvert(
|
|
sizeU: number,
|
|
sizeV: number,
|
|
txU: number,
|
|
txV: number,
|
|
setU: number = 0,
|
|
setV: number = 0,
|
|
invertU = false,
|
|
invertV = false,
|
|
) {
|
|
/*
|
|
* UV coordinate system
|
|
* 0,1 1,1
|
|
* V
|
|
* |
|
|
* |
|
|
* |
|
|
* |
|
|
* +-----------U
|
|
* 0,0 1,0
|
|
*/
|
|
|
|
/*
|
|
* txU equivalent to 1
|
|
* txV equivalent to 1
|
|
*
|
|
* 0 txU
|
|
* 0 -------------------- 1
|
|
* | |
|
|
* setU +sizeU
|
|
* u1 u2
|
|
*/
|
|
|
|
const uStart = convertRange(setU, [0, txU], [0, 1]);
|
|
const uEnd = convertRange(setU + sizeU, [0, txU], [0, 1]);
|
|
const vStart = 1 - convertRange(setV, [0, txV], [0, 1]);
|
|
const vEnd = 1 - convertRange(setV + sizeV, [0, txV], [0, 1]);
|
|
|
|
return new Vector4(
|
|
invertU ? uEnd : uStart,
|
|
invertU ? uStart : uEnd,
|
|
invertV ? vEnd : vStart,
|
|
invertV ? vStart : vEnd,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return instructions on which UV offsets to use when unwrapping a box.
|
|
* @param size Object size
|
|
* @returns Box unwrap instructions
|
|
*/
|
|
static unwrapBox(size: Vector3) {
|
|
const widths = UVMapper.boxFaceOrder.map(
|
|
(item) => size[UVMapper.boxWidthSide[item]],
|
|
);
|
|
const heights = UVMapper.boxFaceOrder.map(
|
|
(item) => size[UVMapper.boxHeightSide[item]],
|
|
);
|
|
const layout: any = {
|
|
// top row
|
|
up: { x: 'west', y: 0 },
|
|
down: { x: 'up', y: 0 },
|
|
// bottom row
|
|
west: { x: 0, y: 'up' },
|
|
south: { x: 'west', y: 'up' },
|
|
east: { x: 'south', y: 'down' },
|
|
north: { x: 'east', y: 'down' },
|
|
};
|
|
|
|
Object.keys(layout).forEach((key) => {
|
|
const entry = layout[key];
|
|
if (typeof entry.x === 'string') {
|
|
const prev = layout[entry.x];
|
|
entry.x = prev.x + widths[UVMapper.boxFaceOrder.indexOf(entry.x)];
|
|
}
|
|
|
|
if (typeof entry.y === 'string') {
|
|
const prev = layout[entry.y];
|
|
entry.y = prev.y + heights[UVMapper.boxFaceOrder.indexOf(entry.y)];
|
|
}
|
|
});
|
|
|
|
return layout;
|
|
}
|
|
|
|
static mapBoxUVs(
|
|
size: Vector3,
|
|
geometry: BoxGeometry,
|
|
texture?: MsonTexture,
|
|
) {
|
|
const uvAttribute = geometry.getAttribute('uv') as BufferAttribute;
|
|
const index = geometry.getIndex() as BufferAttribute;
|
|
const uv = new Vector2();
|
|
|
|
const layout = UVMapper.unwrapBox(size);
|
|
|
|
let textureWidth = texture?.w ?? 64;
|
|
let textureHeight = texture?.h ?? 64;
|
|
let textureOffsetU = texture?.u ?? 0;
|
|
let textureOffsetV = texture?.v ?? 0;
|
|
|
|
// px nx py ny pz nz
|
|
// clockwise count
|
|
for (let i = 0; i < index!.count; i += 3) {
|
|
const faceName = UVMapper.boxFaceOrder[Math.floor(i / 6)];
|
|
const triIndex = Math.ceil((i / 6) % 1);
|
|
const sizeU = size[UVMapper.boxWidthSide[faceName]];
|
|
const sizeV = size[UVMapper.boxHeightSide[faceName]];
|
|
const addVOffset = layout[faceName].y;
|
|
const addUOffset = layout[faceName].x;
|
|
let finalU = textureOffsetU + addUOffset;
|
|
let finalV = textureOffsetV + addVOffset;
|
|
const faceCoords = UVMapper.uvOffsetConvert(
|
|
sizeU,
|
|
sizeV,
|
|
textureWidth,
|
|
textureHeight,
|
|
finalU,
|
|
finalV,
|
|
faceName === 'up' || faceName === 'down',
|
|
faceName === 'up',
|
|
);
|
|
|
|
for (let attrib = 0; attrib < 3; attrib++) {
|
|
const a = index?.getX(i + attrib);
|
|
uv.fromBufferAttribute(uvAttribute, a);
|
|
|
|
// -> bottom left, top left, bottom right
|
|
if (triIndex === 0) {
|
|
if (attrib === 0) uv.set(faceCoords.x, faceCoords.z);
|
|
if (attrib === 1) uv.set(faceCoords.x, faceCoords.w);
|
|
if (attrib === 2) uv.set(faceCoords.y, faceCoords.z);
|
|
}
|
|
|
|
// -> top left, top right, bottom right
|
|
if (triIndex === 1) {
|
|
if (attrib === 0) uv.set(faceCoords.x, faceCoords.w);
|
|
if (attrib === 1) uv.set(faceCoords.y, faceCoords.w);
|
|
if (attrib === 2) uv.set(faceCoords.y, faceCoords.z);
|
|
}
|
|
uvAttribute.setXY(a, uv.x, uv.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
static mapPlanarUvs(
|
|
face: MsonFace,
|
|
size: Vector2,
|
|
geometry: PlaneGeometry,
|
|
texture?: MsonTexture,
|
|
mirror?: boolean | [boolean, boolean],
|
|
) {
|
|
const uvAttribute = geometry.getAttribute('uv') as BufferAttribute;
|
|
const index = geometry.getIndex() as BufferAttribute;
|
|
const uv = new Vector2();
|
|
|
|
let textureWidth = texture?.w ?? 64;
|
|
let textureHeight = texture?.h ?? 64;
|
|
let textureOffsetU = texture?.u ?? 0;
|
|
let textureOffsetV = texture?.v ?? 0;
|
|
|
|
const mirroring =
|
|
mirror !== undefined
|
|
? Array.isArray(mirror)
|
|
? mirror
|
|
: [mirror, mirror]
|
|
: [false, false];
|
|
|
|
if (['up', 'down'].includes(face)) mirroring[0] = !mirroring[0];
|
|
|
|
for (let i = 0; i < index!.count; i += 3) {
|
|
const triIndex = Math.ceil((i / 6) % 1);
|
|
const sizeU = size[UVMapper.planeWidthSide[face]];
|
|
const sizeV = size[UVMapper.planeHeightSide[face]];
|
|
|
|
let finalU = textureOffsetU;
|
|
let finalV = textureOffsetV;
|
|
|
|
const faceCoords = UVMapper.uvOffsetConvert(
|
|
sizeU,
|
|
sizeV,
|
|
textureWidth,
|
|
textureHeight,
|
|
finalU,
|
|
finalV,
|
|
mirroring[0],
|
|
mirroring[1],
|
|
);
|
|
|
|
for (let attrib = 0; attrib < 3; attrib++) {
|
|
const a = index?.getX(i + attrib);
|
|
uv.fromBufferAttribute(uvAttribute, a);
|
|
|
|
// -> bottom left, top left, bottom right
|
|
if (triIndex === 0) {
|
|
if (attrib === 0) uv.set(faceCoords.x, faceCoords.z);
|
|
if (attrib === 1) uv.set(faceCoords.x, faceCoords.w);
|
|
if (attrib === 2) uv.set(faceCoords.y, faceCoords.z);
|
|
}
|
|
|
|
// -> top left, top right, bottom right
|
|
if (triIndex === 1) {
|
|
if (attrib === 0) uv.set(faceCoords.x, faceCoords.w);
|
|
if (attrib === 1) uv.set(faceCoords.y, faceCoords.w);
|
|
if (attrib === 2) uv.set(faceCoords.y, faceCoords.z);
|
|
}
|
|
uvAttribute.setXY(a, uv.x, uv.y);
|
|
}
|
|
}
|
|
}
|
|
}
|