@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
452 lines (342 loc) • 13.2 kB
JavaScript
import { assert } from "../../../../core/assert.js";
/*
Ported from https://github.com/zeux/meshoptimizer/blob/master/src/vcacheoptimizer.cpp
*/
const kCacheSizeMax = 16;
const kValenceMax = 8;
class VertexScoreTable {
cache = new Float32Array(1 + kCacheSizeMax);
live = new Float32Array(1 + kValenceMax);
/**
*
* @param {number[]} cache
* @param {number[]} live
* @returns {VertexScoreTable}
*/
static from(cache, live) {
const r = new VertexScoreTable();
r.live.set(live);
r.cache.set(cache);
return r;
}
}
// Tuned to minimize the ACMR of a GPU that has a cache profile similar to NVidia and AMD
const kVertexScoreTable = VertexScoreTable.from(
[0., 0.779, 0.791, 0.789, 0.981, 0.843, 0.726, 0.847, 0.882, 0.867, 0.799, 0.642, 0.613, 0.600, 0.568, 0.372, 0.234],
[0., 0.995, 0.713, 0.450, 0.404, 0.059, 0.005, 0.147, 0.006]
);
// Tuned to minimize the encoded index buffer size
const kVertexScoreTableStrip = VertexScoreTable.from(
[0., 1.000, 1.000, 1.000, 0.453, 0.561, 0.490, 0.459, 0.179, 0.526, 0.000, 0.227, 0.184, 0.490, 0.112, 0.050, 0.131],
[0., 0.956, 0.786, 0.577, 0.558, 0.618, 0.549, 0.499, 0.489],
);
class TriangleAdjacency {
counts = new Uint32Array(1);
offsets = new Uint32Array(1);
data = new Uint32Array(1);
/**
*
* @param {number} vertex_count
* @param {number} index_count number of faces
*/
allocate(vertex_count, index_count) {
this.counts = new Uint32Array(vertex_count);
this.offsets = new Uint32Array(vertex_count);
this.data = new Uint32Array(index_count);
}
}
/**
*
* @param {TriangleAdjacency} adjacency
* @param {number[]|Uint32Array} indices
* @param {number} index_count
* @param {number} vertex_count
*/
function buildTriangleAdjacency(adjacency, indices, index_count, vertex_count) {
const face_count = index_count / 3;
// allocate arrays
adjacency.allocate(vertex_count, index_count);
// fill triangle counts
// adjacency.counts.fill(0);// unnecessary, new arrays are already zero-filled
const adjacency_counts = adjacency.counts;
for (let i = 0; i < index_count; ++i) {
const index = indices[i];
assert.lessThan(index, vertex_count);
adjacency_counts[index]++;
}
// fill offset table
let offset = 0;
const adjacency_offsets = adjacency.offsets;
for (let i = 0; i < vertex_count; ++i) {
adjacency_offsets[i] = offset;
offset += adjacency_counts[i];
}
assert.equal(offset, index_count);
const adjacency_data = adjacency.data;
// fill triangle data
for (let i = 0; i < face_count; ++i) {
const i3 = i * 3;
const a = indices[i3];
const b = indices[i3 + 1];
const c = indices[i3 + 2];
adjacency_data[adjacency_offsets[a]++] = i;
adjacency_data[adjacency_offsets[b]++] = i;
adjacency_data[adjacency_offsets[c]++] = i;
}
// fix offsets that have been disturbed by the previous pass
for (let i = 0; i < vertex_count; ++i) {
// assert(adjacency.offsets[i] >= adjacency.counts[i]);
adjacency_offsets[i] -= adjacency_counts[i];
}
}
/**
*
* @param {number[]} dead_end
* @param {number} dead_end_top
* @param {number} input_cursor
* @param {number[]} live_triangles
* @param {number} vertex_count
* @returns {number} -1 if not found
*/
function getNextVertexDeadEnd(dead_end, dead_end_top, input_cursor, live_triangles, vertex_count) {
let top = dead_end_top;
let cursor = input_cursor;
// check dead-end stack
while (top) {
const vertex = dead_end[--top];
if (live_triangles[vertex] > 0)
return vertex;
}
// input order
while (cursor < vertex_count) {
if (live_triangles[cursor] > 0)
return cursor;
++cursor;
}
return -1;
}
/**
*
* @param {number[]} candidates
* @param {number} next_candidates_begin
* @param {number} next_candidates_end
* @param {number[]} live_triangles
* @param {number[]} cache_timestamps
* @param {number} timestamp
* @param {number} cache_size
* @returns {number} -1 if no candidate found
*/
function getNextVertexNeighbour(candidates, next_candidates_begin, next_candidates_end, live_triangles, cache_timestamps, timestamp, cache_size) {
let best_candidate = -1;
let best_priority = -1;
for (let next_candidate = next_candidates_begin; next_candidate !== next_candidates_end; ++next_candidate) {
const vertex = candidates[next_candidate];
// otherwise we don't need to process it
if (live_triangles[vertex] > 0) {
let priority = 0;
// will it be in cache after fanning?
if (2 * live_triangles[vertex] + timestamp - cache_timestamps[vertex] <= cache_size) {
priority = timestamp - cache_timestamps[vertex]; // position in cache
}
if (priority > best_priority) {
best_candidate = vertex;
best_priority = priority;
}
}
}
return best_candidate;
}
/**
*
* @param {VertexScoreTable} table
* @param {number} cache_position
* @param {number} live_triangles
* @returns {number}
*/
function vertexScore(table, cache_position, live_triangles) {
assert.greaterThanOrEqual(cache_position, -1);
assert.lessThan(cache_position, kCacheSizeMax);
const live_triangles_clamped = live_triangles < kValenceMax ? live_triangles : kValenceMax;
return table.cache[1 + cache_position] + table.live[live_triangles_clamped];
}
/**
*
* @param {number} input_cursor
* @param {Uint8Array} emitted_flags
* @param {number} face_count
* @returns {number}
*/
function getNextTriangleDeadEnd(input_cursor, emitted_flags, face_count) {
let cursor = input_cursor;
// input order
while (cursor < face_count) {
if (emitted_flags[cursor] === 0) {
return cursor;
}
++cursor;
}
return -1;
}
/**
*
* @param {number[]|Uint32Array} destination
* @param {number[]|Uint32Array} indices
* @param {number} index_count
* @param {number} vertex_count
* @param {VertexScoreTable} table
*/
function meshopt_optimizeVertexCacheTable(destination, indices, index_count, vertex_count, table) {
assert.equal(index_count % 3, 0);
// guard for empty meshes
if (index_count === 0 || vertex_count === 0) {
return;
}
// support in-place optimization
if (destination === indices) {
indices = new Uint32Array(indices);
}
const cache_size = 16;
assert.lessThanOrEqual(cache_size, kCacheSizeMax);
const face_count = index_count / 3;
// build adjacency information
const adjacency = new TriangleAdjacency();
buildTriangleAdjacency(adjacency, indices, index_count, vertex_count);
// live triangle counts
const live_triangles = new Uint32Array(vertex_count);
live_triangles.set(adjacency.counts);
// emitted flags
const emitted_flags = new Uint8Array(face_count);
// memset(emitted_flags, 0, face_count); // no need to zero-fill, already zero-filled by default
// compute initial vertex scores
const vertex_scores = new Float32Array(vertex_count);
for (let i = 0; i < vertex_count; ++i) {
vertex_scores[i] = vertexScore(table, -1, live_triangles[i]);
}
// compute triangle scores
const triangle_scores = new Float32Array(face_count);
for (let i = 0; i < face_count; ++i) {
const i3 = i * 3;
const a = indices[i3];
const b = indices[i3 + 1];
const c = indices[i3 + 2];
triangle_scores[i] = vertex_scores[a] + vertex_scores[b] + vertex_scores[c];
}
let cache = new Uint32Array(kCacheSizeMax + 3);
let cache_new = new Uint32Array(kCacheSizeMax + 3);
let cache_count = 0;
let current_triangle = 0;
let input_cursor = 1;
let output_triangle = 0;
while (current_triangle !== -1) {
assert.lessThan(output_triangle, face_count);
const in_tri_3 = current_triangle * 3;
const a = indices[in_tri_3];
const b = indices[in_tri_3 + 1];
const c = indices[in_tri_3 + 2];
// output indices
const out_tri_3 = output_triangle * 3;
destination[out_tri_3] = a;
destination[out_tri_3 + 1] = b;
destination[out_tri_3 + 2] = c;
output_triangle++;
// update emitted flags
emitted_flags[current_triangle] = true;
triangle_scores[current_triangle] = 0;
// new triangle
let cache_write = 0;
cache_new[cache_write++] = a;
cache_new[cache_write++] = b;
cache_new[cache_write++] = c;
// old triangles
for (let i = 0; i < cache_count; ++i) {
const index = cache[i];
if (index !== a && index !== b && index !== c) {
cache_new[cache_write++] = index;
}
}
// swap caches
const cache_temp = cache;
cache = cache_new;
cache_new = cache_temp;
cache_count = cache_write > cache_size ? cache_size : cache_write;
// update live triangle counts
live_triangles[a]--;
live_triangles[b]--;
live_triangles[c]--;
// remove emitted triangle from adjacency data
// this makes sure that we spend less time traversing these lists on subsequent iterations
for (let k = 0; k < 3; ++k) {
const index = indices[in_tri_3 + k];
const neighbour_offset = adjacency.offsets[index];
const neighbours = adjacency.data;
const neighbours_size = adjacency.counts[index];
for (let i = 0; i < neighbours_size; ++i) {
const tri = neighbours[i + neighbour_offset];
if (tri === current_triangle) {
neighbours[neighbour_offset + i] = neighbours[neighbour_offset + neighbours_size - 1];
adjacency.counts[index]--;
break;
}
}
}
let best_triangle = -1;
let best_score = 0;
// update cache positions, vertex scores and triangle scores, and find next best triangle
for (let i = 0; i < cache_write; ++i) {
const index = cache[i];
const cache_position = i >= cache_size ? -1 : i;
// update vertex score
const score = vertexScore(table, cache_position, live_triangles[index]);
const score_diff = score - vertex_scores[index];
vertex_scores[index] = score;
// update scores of vertex triangles
const neighbours_begin = adjacency.offsets[index];
const neighbours_end = neighbours_begin + adjacency.counts[index];
for (let it = neighbours_begin; it !== neighbours_end; ++it) {
const tri = adjacency.data[it];
assert.equal(emitted_flags[tri], 0);
const tri_score = triangle_scores[tri] + score_diff;
assert.greaterThan(tri_score, 0);
if (best_score < tri_score) {
best_triangle = tri;
best_score = tri_score;
}
triangle_scores[tri] = tri_score;
}
}
// step through input triangles in order if we hit a dead-end
current_triangle = best_triangle;
if (current_triangle === -1) {
// inlined getNextTriangleDeadEnd because of differences between C and JS, namely lack of pointers in JS
while (input_cursor < face_count) {
if (emitted_flags[input_cursor] === 0) {
current_triangle = input_cursor;
break;
}
++input_cursor;
}
}
}
assert.equal(input_cursor, face_count);
assert.equal(output_triangle, face_count);
}
/**
*
* @param {number[]|Uint32Array} destination
* @param {number[]|Uint32Array} indices
* @param {number} index_count
* @param {number} vertex_count
*/
export function meshopt_optimizeVertexCache(destination, indices, index_count, vertex_count) {
meshopt_optimizeVertexCacheTable(destination, indices, index_count, vertex_count, kVertexScoreTable);
}
/**
*
* @param {number[]|Uint32Array} destination
* @param {number[]|Uint32Array} indices
* @param {number} index_count
* @param {number} vertex_count
*/
export function meshopt_optimizeVertexCacheStrip(destination, indices, index_count, vertex_count) {
meshopt_optimizeVertexCacheTable(destination, indices, index_count, vertex_count, kVertexScoreTableStrip);
}