UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

452 lines (342 loc) • 13.2 kB
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); }