@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
340 lines (279 loc) • 10.5 kB
JavaScript
import { vec3 } from "gl-matrix";
import { v3_allocate } from "../../../../core/geom/vec3/v3_allocate.js";
import { DEG_TO_RAD } from "../../../../core/math/DEG_TO_RAD.js";
import { Build4RuleGroups } from "./Build4RuleGroups.js";
import { MARK_DEGENERATE } from "./constants/MARK_DEGENERATE.js";
import { DegenEpilogue } from "./DegenEpilogue.js";
import { DegenPrologue } from "./DegenPrologue.js";
import { GenerateInitialVerticesIndexList } from "./GenerateInitialVerticesIndexList.js";
import { GenerateSharedVerticesIndexList } from "./GenerateSharedVerticesIndexList.js";
import { GenerateTSpaces } from "./GenerateTSpaces.js";
import { GetPosition } from "./GetPosition.js";
import { InitTriInfo } from "./InitTriInfo.js";
import { m_getNumFaces } from "./m_getNumFaces.js";
import { m_setTSpace } from "./m_setTSpace.js";
import { malloc } from "./malloc.js";
import { STSpace } from "./STSpace.js";
/*
To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the
normal map sampler must use the exact inverse of the pixel shader transformation.
The most efficient transformation we can possibly do in the pixel shader is
achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN.
pixel shader (fast transform out)
vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN );
where vNt is the tangent space normal. The normal map sampler must likewise use the
interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader.
sampler does (exact inverse of pixel shader):
float3 row0 = cross(vB, vN);
float3 row1 = cross(vN, vT);
float3 row2 = cross(vT, vB);
float fSign = dot(vT, row0)<0 ? -1 : 1;
vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) );
where vNout is the sampled normal in some chosen 3D space.
Should you choose to reconstruct the bitangent in the pixel shader instead
of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also.
Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of
quads as your renderer then problems will occur since the interpolated tangent spaces will differ
eventhough the vertex level tangent spaces match. This can be solved either by triangulating before
sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier.
However, this must be used both by the sampler and your tools/rendering pipeline.
@see https://github.com/mmikk/MikkTSpace/blob/master/mikktspace.c (the original)
@see https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/intern/mikktspace/mikktspace.c (looks like best-optimized version)
@see https://github.com/undefinist/mikktspacehx/blob/master/src/mikktspace/Mikktspace.hx (Haxe port)
@see https://github.com/gltf-rs/mikktspace/blob/6275cc4f15cff8be29819fb34ae8be3b9129dae1/src/generated.rs (Rust port)
*/
class SGroup {
constructor() {
this.iNrFaces = 0;
/**
* *int
* @type {null}
*/
this.pFaceIndices = null;
this.iVertexRepresentitive = 0;
this.bOrientPreservering = false;
}
}
class STriInfo {
constructor() {
/**
* int[3]
* @type {number[]}
*/
this.FaceNeighbors = [];
/**
* SGroup[3]
* @type {SGroup[]}
*/
this.AssignedGroup = [];
// normalized first order face derivatives
/**
* @type {vec3|Float32Array}
*/
this.vOs = v3_allocate();
/**
* @type {vec3|Float32Array}
*/
this.vOt = v3_allocate();
// original magnitudes
this.fMagS = 0;
this.fMagT = 0;
/**
*
* @type {number}
*/
this.iOrgFaceNumber = 0;
/**
*
* @type {number}
*/
this.iFlag = 0;
/**
* int[4]
* @type {number[]}
*/
this.vert_num = new Uint8Array(4);
/**
*
* @type {number}
*/
this.iTSpacesOffs = 0;
}
}
export class SMikkTSpaceContext {
constructor() {
/**
*
* @type {number[]}
*/
this.geometry_buffer_index = [];
/**
*
* @type {number[]}
*/
this.geometry_buffer_vertex_normal = [];
/**
*
* @type {number[]}
*/
this.geometry_buffer_vertex_position = [];
/**
*
* @type {number[]}
*/
this.geometry_buffer_vertex_uv = [];
/**
*
* @type {number[]}
*/
this.geometry_buffer_vertex_tangent = [];
/**
*
* @type {number[]}
*/
this.geometry_buffer_vertex_bitangent = [];
}
}
// returns the position/normal/texcoord of the referenced face of vertex number iVert.
// iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads.
/**
*
* @param {SMikkTSpaceContext} pContext
* @param {number} [fAngularThreshold]
* @return {boolean}
*/
export function genTangSpace(pContext, fAngularThreshold = 180) {
// count nr_triangles
let iNrTSPaces = 0, iNrMaxGroups = 0;
let iNrActiveGroups = 0;
const iNrFaces = m_getNumFaces(pContext);
const fThresCos = Math.cos(fAngularThreshold * DEG_TO_RAD);
// count triangles on supported faces
// AlexGoldring: since we only support triangles, the number of triangles is going to equal number of faces
let iNrTrianglesIn = iNrFaces;
if (iNrTrianglesIn <= 0) {
return false;
}
// allocate memory for an index list
const piTriListIn = new Int32Array(3 * iNrTrianglesIn);
const pTriInfos = malloc(STriInfo, iNrTrianglesIn);
// make an initial triangle --> face index list
iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn);
// make a welded index list of identical positions and attributes (pos, norm, texc)
//printf("gen welded index list begin\n");
GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn);
//printf("gen welded index list end\n");
// Mark all degenerate triangles
const iTotTris = iNrTrianglesIn;
let iDegenTriangles = 0;
const p0 = vec3.create();
const p1 = vec3.create();
const p2 = vec3.create();
for (let t = 0; t < iTotTris; t++) {
const t3 = t * 3;
const i0 = piTriListIn[t3 ];
const i1 = piTriListIn[t3 + 1];
const i2 = piTriListIn[t3 + 2];
GetPosition(p0, 0, pContext, i0);
GetPosition(p1, 0, pContext, i1);
GetPosition(p2, 0, pContext, i2);
if (
vec3.exactEquals(p0, p1) ||
vec3.exactEquals(p0, p2) ||
vec3.exactEquals(p1, p2)
) {
// degenerate
pTriInfos[t].iFlag |= MARK_DEGENERATE;
++iDegenTriangles;
}
}
iNrTrianglesIn = iTotTris - iDegenTriangles;
// mark all triangle pairs that belong to a quad with only one
// good triangle. These need special treatment in DegenEpilogue().
// Additionally, move all good triangles to the start of
// pTriInfos[] and piTriListIn[] without changing order and
// put the degenerate triangles last.
DegenPrologue(pTriInfos, piTriListIn, iNrTrianglesIn, iTotTris);
// evaluate triangle level attributes and neighbor list
//printf("gen neighbors list begin\n");
InitTriInfo(pTriInfos, piTriListIn, pContext, iNrTrianglesIn);
//printf("gen neighbors list end\n");
// based on the 4 rules, identify groups based on connectivity
iNrMaxGroups = iNrTrianglesIn * 3;
const pGroups = malloc(SGroup, iNrMaxGroups);
const piGroupTrianglesBuffer = new Int32Array(iNrTrianglesIn * 3);
//printf("gen 4rule groups begin\n");
iNrActiveGroups = Build4RuleGroups(
pTriInfos,
pGroups,
piGroupTrianglesBuffer,
piTriListIn,
iNrTrianglesIn
);
//printf("gen 4rule groups end\n");
//
/**
*
* @type {STSpace[]}
*/
const psTspace = malloc(STSpace, iNrTSPaces);
// memset not requried in JS
// memset(psTspace, 0, sizeof(STSpace)*iNrTSPaces);
for (let t = 0; t < iNrTSPaces; t++) {
const stSpace = psTspace[t];
stSpace.vOs[0] = 1.0;
stSpace.vOs[1] = 0.0;
stSpace.vOs[2] = 0.0;
stSpace.fMagS = 1.0;
stSpace.vOt[0] = 0.0;
stSpace.vOt[1] = 1.0;
stSpace.vOt[2] = 0.0;
stSpace.fMagT = 1.0;
}
// make tspaces, each group is split up into subgroups if necessary
// based on fAngularThreshold. Finally, a tangent space is made for
// every resulting subgroup
//printf("gen tspaces begin\n");
GenerateTSpaces(
psTspace,
pTriInfos,
pGroups,
iNrActiveGroups,
piTriListIn,
fThresCos,
pContext
);
//printf("gen tspaces end\n");
// degenerate quads with one good triangle will be fixed by copying a space from
// the good triangle to the coinciding vertex.
// all other degenerate triangles will just copy a space from any good triangle
// with the same welded index in piTriListIn[].
DegenEpilogue(
psTspace,
pTriInfos,
piTriListIn,
pContext,
iNrTrianglesIn,
iTotTris
);
for (let f = 0; f < iNrFaces; f++) {
// set data
for (let i = 0; i < 3; i++) {
const index = f * 3 + i
const pTSpace = psTspace[index];
const tangent = pTSpace.vOs;
const bitangent = pTSpace.vOt;
m_setTSpace(
pContext,
tangent,
bitangent,
pTSpace.fMagS,
pTSpace.fMagT,
pTSpace.bOrient,
f,
i
);
}
}
return true;
}