@awayjs/renderer
Version:
Renderer for AwayJS
880 lines (709 loc) • 22.3 kB
text/typescript
import { Matrix3D, Vector3D, Box, Sphere, Rectangle, Matrix } from '@awayjs/core';
import { AttributesView, Short2Attributes } from '@awayjs/stage';
import { TriangleElements } from '../elements/TriangleElements';
import { GeneratorUtils, MeshView } from './GeneratorUtils';
import { HitTestCache } from './HitTestCache';
export class TriangleElementsUtils {
//TODO - generate this dyanamically based on num tris
public static hitTest(
x: number,
y: number,
_z: number,
box: Box,
triangleElements: TriangleElements,
count: number,
offset: number = 0,
): boolean {
const positionAttributes: AttributesView = triangleElements.positions;
const curveAttributes: AttributesView = triangleElements.getCustomAtributes('curves');
const posStride: number = positionAttributes.stride;
const curveStride: number = curveAttributes ? curveAttributes.stride : null;
let positions: ArrayBufferView = positionAttributes.get(count, offset);
let curves: ArrayBufferView = curveAttributes ? curveAttributes.get(count, offset) : null;
let indices: Uint16Array;
let len: number;
if (triangleElements.indices) {
indices = triangleElements.indices.get(count, offset);
positions = positionAttributes.get(positionAttributes.count);
curves = curveAttributes ? curveAttributes.get(curveAttributes.count) : null;
len = count * triangleElements.indices.dimensions;
} else {
positions = positionAttributes.get(count, offset);
curves = curveAttributes ? curveAttributes.get(count, offset) : null;
len = count;
}
let id0: number;
let id1: number;
let id2: number;
let ax: number;
let ay: number;
let bx: number;
let by: number;
let cx: number;
let cy: number;
const hitTestCache: HitTestCache =
triangleElements.hitTestCache[offset] || (triangleElements.hitTestCache[offset] = new HitTestCache());
const index: number = hitTestCache.lastCollisionIndex;
if (index != -1 && index < len) {
precheck: {
if (indices) {
id0 = indices[index + 2];
id1 = indices[index + 1];
id2 = indices[index];
} else {
id0 = index + 2;
id1 = index + 1;
id2 = index;
}
ax = positions[id0 * posStride];
ay = positions[id0 * posStride + 1];
bx = positions[id1 * posStride];
by = positions[id1 * posStride + 1];
cx = positions[id2 * posStride];
cy = positions[id2 * posStride + 1];
//from a to p
let dx = ax - x;
let dy = ay - y;
//edge normal (a-b)
let nx = by - ay;
let ny = -(bx - ax);
let dot = dx * nx + dy * ny;
if (dot > 0) break precheck;
dx = bx - x;
dy = by - y;
nx = cy - by;
ny = -(cx - bx);
dot = dx * nx + dy * ny;
if (dot > 0) break precheck;
dx = cx - x;
dy = cy - y;
nx = ay - cy;
ny = -(ax - cx);
dot = dx * nx + dy * ny;
if (dot > 0) break precheck;
if (curves) {
//check if not solid
if (curves[id0 * curveStride + 2] != -128) {
const v0x = bx - ax;
const v0y = by - ay;
const v1x = cx - ax;
const v1y = cy - ay;
const v2x = x - ax;
const v2y = y - ay;
const den = v0x * v1y - v1x * v0y;
const v = (v2x * v1y - v1x * v2y) / den;
const w = (v0x * v2y - v2x * v0y) / den;
//var u:number = 1 - v - w; //commented out as inlined away
//here be dragons
const uu = 0.5 * v + w;
const vv = w;
const d = uu * uu - vv;
const az = curves[id0 * curveStride];
if (d > 0 && az == -128) {
break precheck;
} else if (d < 0 && az == 127) {
break precheck;
}
}
}
return true;
}
}
//hard coded min vertex count to bother using a grid for
if (len > 150) {
const cells: Array<Array<number>> = hitTestCache.cells;
const divisions: number = cells.length
? hitTestCache.divisions
: (hitTestCache.divisions = Math.min(Math.ceil(Math.sqrt(len)), 32));
const conversionX: number = divisions / box.width;
const conversionY: number = divisions / box.height;
const minx: number = box.x;
const miny: number = box.y;
if (!cells.length) {
//build grid
//now we have bounds start creating grid cells and filling
cells.length = divisions * divisions;
for (let k = 0; k < len; k += 3) {
if (indices) {
id0 = indices[k + 2];
id1 = indices[k + 1];
id2 = indices[k];
} else {
id0 = k + 2;
id1 = k + 1;
id2 = k;
}
ax = positions[id0 * posStride];
ay = positions[id0 * posStride + 1];
bx = positions[id1 * posStride];
by = positions[id1 * posStride + 1];
cx = positions[id2 * posStride];
cy = positions[id2 * posStride + 1];
//subtractions to push into positive space
const min_index_x: number = Math.floor((Math.min(ax, bx, cx) - minx) * conversionX);
const min_index_y: number = Math.floor((Math.min(ay, by, cy) - miny) * conversionY);
const max_index_x: number = Math.floor((Math.max(ax, bx, cx) - minx) * conversionX);
const max_index_y: number = Math.floor((Math.max(ay, by, cy) - miny) * conversionY);
for (let i: number = min_index_x; i <= max_index_x; i++) {
for (let j: number = min_index_y; j <= max_index_y; j++) {
const c: number = i + j * divisions;
const nodes: Array<number> = cells[c] || (cells[c] = new Array<number>());
//push in the triangle ids
nodes.push(k);
}
}
}
}
const index_x: number = Math.floor((x - minx) * conversionX);
const index_y: number = Math.floor((y - miny) * conversionY);
const nodes: Array<number> = cells[index_x + index_y * divisions];
if (nodes == null) {
hitTestCache.lastCollisionIndex = -1;
return false;
}
const nodeCount = nodes.length;
for (let n = 0; n < nodeCount; n++) {
const k = nodes[n];
if (indices) {
id2 = indices[k];
} else {
id2 = k;
}
if (id2 == index) continue;
if (indices) {
id0 = indices[k + 2];
id1 = indices[k + 1];
} else {
id0 = k + 2;
id1 = k + 1;
}
ax = positions[id0 * posStride];
ay = positions[id0 * posStride + 1];
bx = positions[id1 * posStride];
by = positions[id1 * posStride + 1];
cx = positions[id2 * posStride];
cy = positions[id2 * posStride + 1];
//from a to p
let dx = ax - x;
let dy = ay - y;
//edge normal (a-b)
let nx = by - ay;
let ny = -(bx - ax);
let dot = dx * nx + dy * ny;
if (dot > 0) continue;
dx = bx - x;
dy = by - y;
nx = cy - by;
ny = -(cx - bx);
dot = dx * nx + dy * ny;
if (dot > 0) continue;
dx = cx - x;
dy = cy - y;
nx = ay - cy;
ny = -(ax - cx);
dot = dx * nx + dy * ny;
if (dot > 0) continue;
if (curves) {
//check if not solid
if (curves[id0 * curveStride + 2] != -128) {
const v0x = bx - ax;
const v0y = by - ay;
const v1x = cx - ax;
const v1y = cy - ay;
const v2x = x - ax;
const v2y = y - ay;
const den = v0x * v1y - v1x * v0y;
const v = (v2x * v1y - v1x * v2y) / den;
const w = (v0x * v2y - v2x * v0y) / den;
//var u:number = 1 - v - w; //commented out as inlined away
//here be dragons
const uu = 0.5 * v + w;
const vv = w;
const d = uu * uu - vv;
const az = curves[id0 * curveStride];
if (d > 0 && az == -128) {
continue;
} else if (d < 0 && az == 127) {
continue;
}
}
}
hitTestCache.lastCollisionIndex = k;
return true;
}
hitTestCache.lastCollisionIndex = -1;
return false;
}
//brute force
for (let k = 0; k < len; k += 3) {
if (indices) {
id2 = indices[k];
} else {
id2 = k;
}
if (id2 == index) continue;
if (indices) {
id0 = indices[k + 2];
id1 = indices[k + 1];
} else {
id0 = k + 2;
id1 = k + 1;
}
ax = positions[id0 * posStride];
ay = positions[id0 * posStride + 1];
bx = positions[id1 * posStride];
by = positions[id1 * posStride + 1];
cx = positions[id2 * posStride];
cy = positions[id2 * posStride + 1];
//from a to p
let dx = ax - x;
let dy = ay - y;
//edge normal (a-b)
let nx = by - ay;
let ny = -(bx - ax);
let dot = dx * nx + dy * ny;
if (dot > 0) continue;
dx = bx - x;
dy = by - y;
nx = cy - by;
ny = -(cx - bx);
dot = dx * nx + dy * ny;
if (dot > 0) continue;
dx = cx - x;
dy = cy - y;
nx = ay - cy;
ny = -(ax - cx);
dot = dx * nx + dy * ny;
if (dot > 0) continue;
if (curves) {
//check if not solid
if (curves[id0 * curveStride + 2] != -128) {
const v0x = bx - ax;
const v0y = by - ay;
const v1x = cx - ax;
const v1y = cy - ay;
const v2x = x - ax;
const v2y = y - ay;
const den = v0x * v1y - v1x * v0y;
const v = (v2x * v1y - v1x * v2y) / den;
const w = (v0x * v2y - v2x * v0y) / den;
//var u:number = 1 - v - w; //commented out as inlined away
//here be dragons
const uu = 0.5 * v + w;
const vv = w;
const d = uu * uu - vv;
const az = curves[id0 * curveStride];
if (d > 0 && az == -128) {
continue;
} else if (d < 0 && az == 127) {
continue;
}
}
}
hitTestCache.lastCollisionIndex = k;
return true;
}
hitTestCache.lastCollisionIndex = -1;
return false;
}
public static getBoxBounds(
positionAttributes: AttributesView,
indexAttributes: Short2Attributes,
matrix3D: Matrix3D,
cache: Box,
target: Box,
count: number,
offset: number = 0,
): Box {
let positions: ArrayBufferView;
const posDim: number = positionAttributes.dimensions;
const posStride: number = positionAttributes.stride;
let minX: number = 0,
minY: number = 0,
minZ: number = 0;
let maxX: number = 0,
maxY: number = 0,
maxZ: number = 0;
let indices: Uint16Array;
let len: number;
if (indexAttributes) {
len = count * indexAttributes.dimensions;
indices = indexAttributes.get(count, offset);
positions = positionAttributes.get(positionAttributes.count);
} else {
len = count;
positions = positionAttributes.get(count, offset);
}
if (len == 0) return target;
let i: number = 0;
let index: number;
let pos1: number, pos2: number, pos3: number, rawData: Float32Array;
if (matrix3D) rawData = matrix3D._rawData;
if (target == null) {
target = cache || new Box();
index = indices ? indices[i] * posStride : i * posStride;
if (matrix3D) {
if (posDim == 3) {
pos1 =
positions[index] * rawData[0] +
positions[index + 1] * rawData[4] +
positions[index + 2] * rawData[8] +
rawData[12];
pos2 =
positions[index] * rawData[1] +
positions[index + 1] * rawData[5] +
positions[index + 2] * rawData[9] +
rawData[13];
pos3 =
positions[index] * rawData[2] +
positions[index + 1] * rawData[6] +
positions[index + 2] * rawData[10] +
rawData[14];
} else {
pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + rawData[12];
pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + rawData[13];
}
} else {
pos1 = positions[index];
pos2 = positions[index + 1];
pos3 = posDim == 3 ? positions[index + 2] : 0;
}
maxX = minX = pos1;
maxY = minY = pos2;
maxZ = minZ = posDim == 3 ? pos3 : 0;
i++;
} else {
maxX = (minX = target.x) + target.width;
maxY = (minY = target.y) + target.height;
maxZ = (minZ = target.z) + target.depth;
}
for (; i < len; i++) {
index = indices ? indices[i] * posStride : i * posStride;
if (matrix3D) {
if (posDim == 3) {
pos1 =
positions[index] * rawData[0] +
positions[index + 1] * rawData[4] +
positions[index + 2] * rawData[8] +
rawData[12];
pos2 =
positions[index] * rawData[1] +
positions[index + 1] * rawData[5] +
positions[index + 2] * rawData[9] +
rawData[13];
pos3 =
positions[index] * rawData[2] +
positions[index + 1] * rawData[6] +
positions[index + 2] * rawData[10] +
rawData[14];
} else {
pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + rawData[12];
pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + rawData[13];
}
} else {
pos1 = positions[index];
pos2 = positions[index + 1];
pos3 = posDim == 3 ? positions[index + 2] : 0;
}
if (pos1 < minX) minX = pos1;
else if (pos1 > maxX) maxX = pos1;
if (pos2 < minY) minY = pos2;
else if (pos2 > maxY) maxY = pos2;
if (posDim == 3) {
if (pos3 < minZ) minZ = pos3;
else if (pos3 > maxZ) maxZ = pos3;
}
}
target.width = maxX - (target.x = minX);
target.height = maxY - (target.y = minY);
target.depth = maxZ - (target.z = minZ);
return target;
}
public static getSphereBounds(
positionAttributes: AttributesView,
center: Vector3D,
_matrix3D: Matrix3D,
_cache: Sphere,
output: Sphere,
count: number,
offset: number = 0,
): Sphere {
const positions: ArrayBufferView = positionAttributes.get(count, offset);
const posDim: number = positionAttributes.dimensions;
const posStride: number = positionAttributes.stride;
let maxRadiusSquared: number = 0;
let radiusSquared: number;
const len = count * posStride;
let distanceX: number;
let distanceY: number;
let distanceZ: number;
for (let i: number = 0; i < len; i += posStride) {
distanceX = positions[i] - center.x;
distanceY = positions[i + 1] - center.y;
distanceZ = posDim == 3 ? positions[i + 2] - center.z : -center.z;
radiusSquared = distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ;
if (maxRadiusSquared < radiusSquared) maxRadiusSquared = radiusSquared;
}
if (output == null) output = new Sphere();
output.x = center.x;
output.y = center.y;
output.z = center.z;
output.radius = Math.sqrt(maxRadiusSquared);
return output;
}
public static prepareScale9 (
elem: TriangleElements,
bounds: Rectangle,
grid: Rectangle,
copy: boolean,
emitUV?: boolean,
uvMatrix?: Matrix
): TriangleElements {
if (elem._numElements !== 0) {
throw 'Indices not support yet';
}
const target = copy ? elem.clone() : elem;
const shapeBounds = TriangleElementsUtils.getBoxBounds (
elem.positions,
elem.indices,
null,null, null,
elem._numElements | elem._numVertices, 0);
const sliceX = [
-Infinity,
grid.x,
grid.right,
Infinity
];
const sliceY = [
-Infinity,
grid.y,
grid.bottom,
Infinity
];
const chunkX = {
from: 0, to: 0
};
const chunkY = {
from: 0, to: 0
};
for (let i = 1; i < 3; i++) {
if (shapeBounds.x >= sliceX[i]) {
chunkX.from = chunkX.to = i;
}
if (shapeBounds.y >= sliceY[i]) {
chunkY.from = chunkY.to = i;
}
}
for (let i = 0; i < 3; i++) {
if (shapeBounds.right > sliceX[i] && i >= chunkX.from) {
chunkX.to = i;
}
if (shapeBounds.bottom > sliceY[i] && i >= chunkY.from) {
chunkY.to = i;
}
}
const indices = Array.from({ length: 9 }, (_) => 0);
target.scale9Indices = indices;
target.scale9Grid = grid;
target.originalScale9Bounds = bounds;
if (emitUV) {
if (target.positions.dimensions !== 2) {
throw 'Emit UV support only 2D position buffer';
}
const uv = (<Float32Array> target.positions.get(target._numVertices, 0)).slice(0);
const raw = uvMatrix.rawData;
for (let i = 0; i < uv.length; i += 2) {
const x = uv[i + 0];
const y = uv[i + 1];
uv[i + 0] = x * raw[0] + y * raw[2] + raw[4];
uv[i + 1] = x * raw[1] + y * raw[3] + raw[5];
}
target.setUVs(uv);
target.invalidate();
}
// shape already in valid region
// not require run slicer for this case
if (chunkX.from === chunkX.to && chunkY.from === chunkY.to) {
target.scale9Indices[chunkY.from * 3 + chunkX.from] = target._numVertices;
return target;
}
// run splitter
const attrs = target.uvs ? [target.positions, target.uvs] : [target.positions];
let mesh = MeshView.fromAttributes<number>(attrs, target._numVertices, 3);
const vector = new Vector3D(0,0);
vector.setTo(1, 0, 0, 0);
// we slice only over 2 offsets
for (let i = chunkX.from; i < chunkX.to; i++) {
//mesh = GeneratorUtils.SliceHodgman(mesh, vector, sliceX[i]);
mesh = GeneratorUtils.SliceAllNaive(mesh, vector, sliceX[i + 1]);
}
vector.setTo(0, 1, 0, 0);
// we slice only over 2 offsets
for (let i = chunkY.from; i < chunkY.to; i++) {
// generate errors
// mesh = GeneratorUtils.SliceHodgman(mesh, vector, sliceY[i]);
// Naive is more stable, but unoptimal
mesh = GeneratorUtils.SliceAllNaive(mesh, vector, sliceY[i + 1]);
}
mesh.normalise();
// ordering - determine chunk id
for (const p of mesh.poly) {
for (let i = 0; i < 9; i++) {
// over middle point
// because all polygons should be inside rect, all their points should too,
// we can use any points to chek chunk
// but use middle point to redice slicing errors
// BUT this is not garanteed stable results
const data = p.middle.getData(0);
const px = data[0];
const py = data[1];
if (
(px >= sliceX[i % 3]) &&
(px < sliceX[i % 3 + 1]) &&
(py >= sliceY[i / 3 | 0]) &&
(py < sliceY[(i / 3 | 0) + 1])
) {
// slicer can not generate user data
p.userData = i;
}
}
}
mesh.poly.sort((a, b) => a.userData - b.userData);
for (let i = 0; i < mesh.poly.length; i++) {
const index = mesh.poly[i].userData;
indices[index] = i * 3 + 3;
}
// pos
const pos = mesh.toFloatArray(0);
target.setPositions(pos);
if (target.uvs) {
const uvs = mesh.toFloatArray(1);
target.setUVs(uvs);
}
//throw '[TriangleElementUtils] Not implemented';
target.invalidate();
return target;
}
public static updateScale9(
elem: TriangleElements,
originalRect: Rectangle,
scaleX: number,
scaleY: number,
init: boolean = false,
copy: boolean = false,
): TriangleElements {
// todo: for now this only works for Float2Attributes.
if (elem.scale9Indices.length !== 9) {
throw 'ElementUtils: Error - triangleElement does not provide valid slice9Indices!';
}
const offsets = elem.scale9Grid;
const left = offsets.x - originalRect.x;
const right = originalRect.right - offsets.right;
const top = offsets.y - originalRect.y;
const bottom = originalRect.bottom - offsets.bottom;
const s_len = elem.scale9Indices.length;
let innerWidth = originalRect.width * scaleX - (left + right);
let innerHeight = originalRect.height * scaleY - (top + bottom);
let cornerScaleX = 1;
let cornerScaleY = 1;
// reduce a overflow, when scale to small
if (innerWidth < 0) {
innerWidth = 0;
cornerScaleX = originalRect.width * scaleX / (left + right);
}
if (innerHeight < 0) {
innerHeight = 0;
cornerScaleY = originalRect.height * scaleY / (top + bottom);
}
const innerScaleX = innerWidth / offsets.width;
const innerScaleY = innerHeight / offsets.height;
const stride = elem.positions.stride;
const attrOffset = elem.positions.offset;
let newElem: TriangleElements;
let positions: ArrayBufferView;
if (copy) {
// there are not garaties that a buffer is 2 and not has stride
// and a element may has UV
// should working
newElem = elem.clone();
newElem.scale9Grid = elem.scale9Grid;
newElem.initialScale9Positions = elem.initialScale9Positions;
newElem.scale9Indices = elem.scale9Indices;
positions = newElem.positions.get(newElem._numVertices);
} else {
positions = elem.positions.get(elem._numVertices);
}
// todo: i had trouble when just cloning the positions
// for now i just create the initialSlice9Positions by iterating the positions
let initPos: number[];
if (init || !elem.initialScale9Positions) {
initPos = [];
// we store only XY, but buffer can be XYZ
initPos.length = elem._numVertices * 2;
let vindex = 0;
const len = elem.positions.length;
for (let i = 0; i < len; i += stride) {
initPos[vindex + 0] = positions[attrOffset + i + 0];
initPos[vindex + 1] = positions[attrOffset + i + 1];
vindex += 2;
}
elem.initialScale9Positions = initPos;
} else {
initPos = <number[]>elem.initialScale9Positions;
}
const slice9Indices: number[] = elem.scale9Indices;
const slice9Offsets_x = [
0,
left * cornerScaleX - left * innerScaleX,
innerWidth - offsets.width * cornerScaleX,
];
const slice9Offsets_y = [
0,
top * cornerScaleY - top * innerScaleY,
innerHeight - offsets.height * cornerScaleY,
];
// internal buffer iterator
let attrindex = attrOffset;
let vindex = 0;
// iterating over the 9 chunks - keep in mind that we are constructing a 3x3 grid:
for (let s = 0; s < s_len; s++) {
const row = s / 3 | 0;
const col = s % 3;
// only need to x-scale if this is the middle column
// if the innerWidth<=0 we can skip this complete column
const scalex = col === 1 ? innerScaleX : cornerScaleX;
// only need to y-scale if this is the middle row
// if the innerHeight<=0 we can skip this complete row
const scaley = row === 1 ? innerScaleY : cornerScaleY;
// offsetx is different for each column
const offsetx = slice9Offsets_x[col];
// offsety is different for each row
const offsety = slice9Offsets_y[row];
// iterate the verts and apply the translation / scale
// slice9Indices is vertices indeces, is not attribute indices
while (vindex < slice9Indices[s]) {
let vx = initPos[vindex * 2 + 0] - originalRect.x;
let vy = initPos[vindex * 2 + 1] - originalRect.y;
vx = offsetx + vx * scalex;
vy = offsety + vy * scaley;
vx /= scaleX;
vy /= scaleY;
positions[attrindex + 0] = vx + originalRect.x;
positions[attrindex + 1] = vy + originalRect.y;
// we should include a stride, because buffer maybe be contecated
// or XYZ instead of XY
attrindex += stride;
vindex++;
}
}
//console.log("positions",positions);
if (copy) {
newElem.positions.invalidate();
newElem.invalidate();
return newElem;
}
elem.positions.invalidate();
elem.invalidate();
return elem;
}
}