three
Version:
JavaScript 3D library
766 lines (507 loc) • 22.5 kB
JavaScript
/*!
* Copyright (C) 2011 by Morten S. Mikkelsen
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/
// JavaScript port, altered from the MikkTSpace reference implementation:
// https://github.com/mmikk/MikkTSpace/tree/3e895b49d05ea07e4c2133156cfa94369e19e409
//
// Specializes the reference algorithm for unindexed triangles, the default
// 180-degree threshold, and basic tangent/sign output. Attribute welding,
// connected orientation groups, angular subgroups, corner-angle weighting,
// and degenerate-triangle fallback follow the reference.
//
// Hash tables replace sorting and linear degenerate lookups. Unlike the
// reference's BuildNeighborsFast(), every edge bucket is processed: its two
// secondary sorting passes omit their final buckets and can miss neighbors.
// This correction can change tangents for affected meshes.
// Retained for compatibility with the asynchronous WASM implementation.
export const isReady = true;
// Backwards compatibility. Initialization is no longer required.
export const ready = Promise.resolve();
/**
* Retained for API compatibility. There are no persistent resources to release.
*/
export function dispose() {}
// Preserve the reference's float32 rounding and FLT_MIN zero tests. In
// particular, rounding determines membership at the angular subgroup boundary.
const fround = Math.fround;
const FLOAT_MIN = 1.1754943508222875e-38;
const GROUP_WITH_ANY = 4;
const ORIENTATION_PRESERVING = 8;
function dot( ax, ay, az, bx, by, bz ) {
return fround( fround( fround( ax * bx ) + fround( ay * by ) ) + fround( az * bz ) );
}
function vectorLength( x, y, z ) {
return fround( Math.sqrt( dot( x, y, z, x, y, z ) ) );
}
function isNonZero( x, y, z ) {
return Math.abs( x ) > FLOAT_MIN || Math.abs( y ) > FLOAT_MIN || Math.abs( z ) > FLOAT_MIN;
}
function getHashTableSize( vertexCount ) {
let n = 16;
while ( n < vertexCount * 2 ) {
n *= 2;
}
return n;
}
function weldVertices( position, normal, texcoord, vertexCount ) {
const positionBits = new Uint32Array( position.buffer, position.byteOffset, position.length );
const normalBits = new Uint32Array( normal.buffer, normal.byteOffset, normal.length );
const texcoordBits = new Uint32Array( texcoord.buffer, texcoord.byteOffset, texcoord.length );
const table = new Int32Array( getHashTableSize( vertexCount ) );
const mask = table.length - 1;
const result = new Int32Array( vertexCount );
for ( let v = 0; v < vertexCount; v ++ ) {
const p = v * 3;
const t = v * 2;
let hash = 2166136261;
for ( let c = 0; c < 3; c ++ ) {
// Equality in the reference treats positive and negative zero alike.
hash = Math.imul( hash ^ ( position[ p + c ] === 0 ? 0 : positionBits[ p + c ] ), 16777619 );
hash = Math.imul( hash ^ ( normal[ p + c ] === 0 ? 0 : normalBits[ p + c ] ), 16777619 );
}
for ( let c = 0; c < 2; c ++ ) {
hash = Math.imul( hash ^ ( texcoord[ t + c ] === 0 ? 0 : texcoordBits[ t + c ] ), 16777619 );
}
hash ^= hash >>> 16;
let slot = hash & mask;
while ( table[ slot ] ) {
const r = table[ slot ] - 1;
const q = r * 3;
const u = r * 2;
if ( position[ p ] === position[ q ] && position[ p + 1 ] === position[ q + 1 ] && position[ p + 2 ] === position[ q + 2 ] &&
normal[ p ] === normal[ q ] && normal[ p + 1 ] === normal[ q + 1 ] && normal[ p + 2 ] === normal[ q + 2 ] &&
texcoord[ t ] === texcoord[ u ] && texcoord[ t + 1 ] === texcoord[ u + 1 ] ) {
break;
}
slot = ( slot + 1 ) & mask;
}
if ( ! table[ slot ] ) {
table[ slot ] = v + 1;
}
result[ v ] = table[ slot ] - 1;
}
return result;
}
/**
* Generates MikkTSpace tangents for unindexed triangles using the default
* 180-degree angular threshold. Normals are expected to be normalized and all
* input components finite. Extreme values may overflow float32 intermediates,
* as in the reference implementation. Input attributes are not modified.
*
* @param {Float32Array} position - Triangle vertex positions, three components per vertex.
* @param {Float32Array} normal - Vertex normals, three components per vertex.
* @param {Float32Array} texcoord - Texture coordinates, two components per vertex.
* @return {Float32Array} Tangents with four components per vertex: XYZ and handedness.
*/
export function generateTangents( position, normal, texcoord ) {
if ( ! ( position instanceof Float32Array ) || ! ( normal instanceof Float32Array ) || ! ( texcoord instanceof Float32Array ) ) {
throw new TypeError( 'THREE.MikkTSpace: Expected Float32Array inputs.' );
}
const vertexCount = position.length / 3;
if ( position.length % 9 || normal.length !== position.length || texcoord.length !== vertexCount * 2 ) {
throw new Error( 'THREE.MikkTSpace: Expected matching unindexed triangle attributes.' );
}
const output = new Float32Array( vertexCount * 4 );
for ( let v = 0; v < vertexCount; v ++ ) {
output[ v * 4 ] = 1;
output[ v * 4 + 3 ] = - 1;
}
if ( vertexCount === 0 ) {
return output;
}
const weldedIndices = weldVertices( position, normal, texcoord, vertexCount );
const triangleCount = vertexCount / 3;
const triangleVertices = new Int32Array( vertexCount );
const originalTriangles = new Int32Array( triangleCount );
// Remove triangles with coincident positions, preserving the order of
// surviving triangles. Their tangents are copied from surviving vertices
// after all connected groups have been evaluated.
const degenerateTriangles = new Uint8Array( triangleCount );
let activeTriangleCount = 0;
for ( let f = 0; f < triangleCount; f ++ ) {
const a = f * 9;
const b = a + 3;
const c = a + 6;
const sameAB = position[ a ] === position[ b ] && position[ a + 1 ] === position[ b + 1 ] && position[ a + 2 ] === position[ b + 2 ];
const sameAC = position[ a ] === position[ c ] && position[ a + 1 ] === position[ c + 1 ] && position[ a + 2 ] === position[ c + 2 ];
const sameBC = position[ b ] === position[ c ] && position[ b + 1 ] === position[ c + 1 ] && position[ b + 2 ] === position[ c + 2 ];
if ( sameAB || sameAC || sameBC ) {
degenerateTriangles[ f ] = 1;
continue;
}
originalTriangles[ activeTriangleCount ] = f;
triangleVertices[ activeTriangleCount * 3 ] = weldedIndices[ f * 3 ];
triangleVertices[ activeTriangleCount * 3 + 1 ] = weldedIndices[ f * 3 + 1 ];
triangleVertices[ activeTriangleCount * 3 + 2 ] = weldedIndices[ f * 3 + 2 ];
activeTriangleCount ++;
}
const cornerCount = activeTriangleCount * 3;
const triangleFlags = new Uint8Array( activeTriangleCount );
const faceDerivatives = computeFaceDerivatives( triangleVertices, activeTriangleCount, position, texcoord, triangleFlags );
const neighbors = buildNeighbors( triangleVertices, cornerCount );
generateTangentSpaces( triangleVertices, originalTriangles, triangleFlags, faceDerivatives, neighbors, position, normal, output, cornerCount );
if ( activeTriangleCount !== triangleCount ) {
// Match the reference's first surviving corner with identical attributes.
const firstTangent = new Int32Array( vertexCount );
firstTangent.fill( - 1 );
for ( let c = 0; c < cornerCount; c ++ ) {
if ( firstTangent[ triangleVertices[ c ] ] === - 1 ) {
firstTangent[ triangleVertices[ c ] ] = ( originalTriangles[ c / 3 | 0 ] * 3 + c % 3 ) * 4;
}
}
for ( let f = 0; f < triangleCount; f ++ ) {
if ( ! degenerateTriangles[ f ] ) {
continue;
}
for ( let c = 0; c < 3; c ++ ) {
const v = f * 3 + c;
const src = firstTangent[ weldedIndices[ v ] ];
const dst = v * 4;
if ( src >= 0 ) {
for ( let i = 0; i < 4; i ++ ) {
output[ dst + i ] = output[ src + i ];
}
}
}
}
}
return output;
}
function computeFaceDerivatives( triangleVertices, activeTriangleCount, position, texcoord, triangleFlags ) {
const faceDerivatives = new Float32Array( activeTriangleCount * 6 );
for ( let f = 0; f < activeTriangleCount; f ++ ) {
const a = triangleVertices[ f * 3 ];
const b = triangleVertices[ f * 3 + 1 ];
const c = triangleVertices[ f * 3 + 2 ];
const x1 = fround( texcoord[ b * 2 ] - texcoord[ a * 2 ] );
const y1 = fround( texcoord[ b * 2 + 1 ] - texcoord[ a * 2 + 1 ] );
const x2 = fround( texcoord[ c * 2 ] - texcoord[ a * 2 ] );
const y2 = fround( texcoord[ c * 2 + 1 ] - texcoord[ a * 2 + 1 ] );
const dx1 = fround( position[ b * 3 ] - position[ a * 3 ] );
const dy1 = fround( position[ b * 3 + 1 ] - position[ a * 3 + 1 ] );
const dz1 = fround( position[ b * 3 + 2 ] - position[ a * 3 + 2 ] );
const dx2 = fround( position[ c * 3 ] - position[ a * 3 ] );
const dy2 = fround( position[ c * 3 + 1 ] - position[ a * 3 + 1 ] );
const dz2 = fround( position[ c * 3 + 2 ] - position[ a * 3 + 2 ] );
const area = fround( fround( x1 * y2 ) - fround( y1 * x2 ) );
triangleFlags[ f ] = GROUP_WITH_ANY | ( area > 0 ? ORIENTATION_PRESERVING : 0 );
if ( ! ( Math.abs( area ) > FLOAT_MIN ) ) {
continue;
}
const sx = fround( fround( y2 * dx1 ) - fround( y1 * dx2 ) );
const sy = fround( fround( y2 * dy1 ) - fround( y1 * dy2 ) );
const sz = fround( fround( y2 * dz1 ) - fround( y1 * dz2 ) );
const tx = fround( fround( - x2 * dx1 ) + fround( x1 * dx2 ) );
const ty = fround( fround( - x2 * dy1 ) + fround( x1 * dy2 ) );
const tz = fround( fround( - x2 * dz1 ) + fround( x1 * dz2 ) );
const sl = vectorLength( sx, sy, sz );
const tl = vectorLength( tx, ty, tz );
const sign = area > 0 ? 1 : - 1;
const d = f * 6;
if ( Math.abs( sl ) > FLOAT_MIN ) {
const scale = fround( sign / sl );
faceDerivatives[ d ] = sx * scale;
faceDerivatives[ d + 1 ] = sy * scale;
faceDerivatives[ d + 2 ] = sz * scale;
}
if ( Math.abs( tl ) > FLOAT_MIN ) {
const scale = fround( sign / tl );
faceDerivatives[ d + 3 ] = tx * scale;
faceDerivatives[ d + 4 ] = ty * scale;
faceDerivatives[ d + 5 ] = tz * scale;
}
if ( Math.abs( fround( sl / Math.abs( area ) ) ) > FLOAT_MIN && Math.abs( fround( tl / Math.abs( area ) ) ) > FLOAT_MIN ) {
triangleFlags[ f ] &= ~ GROUP_WITH_ANY;
}
}
return faceDerivatives;
}
function buildNeighbors( triangleVertices, cornerCount ) {
// Prepend edges in reverse order so occurrences remain in ascending face order.
const neighbors = new Int32Array( cornerCount );
neighbors.fill( - 1 );
const next = new Int32Array( cornerCount );
const table = new Int32Array( getHashTableSize( cornerCount ) );
const mask = table.length - 1;
for ( let e = cornerCount - 1; e >= 0; e -- ) {
const base = e - e % 3;
const end = base + ( e + 1 ) % 3;
const a = triangleVertices[ e ];
const b = triangleVertices[ end ];
const lo = Math.min( a, b );
const hi = Math.max( a, b );
let h = Math.imul( lo, 0x9e3779b1 ) ^ Math.imul( hi, 0x85ebca6b );
h ^= h >>> 16;
let slot = h & mask;
while ( table[ slot ] ) {
const edge = table[ slot ] - 1;
const edgeEnd = edge - edge % 3 + ( edge + 1 ) % 3;
const x = triangleVertices[ edge ];
const y = triangleVertices[ edgeEnd ];
if ( lo === Math.min( x, y ) && hi === Math.max( x, y ) ) {
break;
}
slot = ( slot + 1 ) & mask;
}
next[ e ] = table[ slot ] - 1;
table[ slot ] = e + 1;
}
for ( let e = 0; e < cornerCount; e ++ ) {
if ( neighbors[ e ] !== - 1 ) {
continue;
}
const base = e - e % 3;
const a = triangleVertices[ e ];
const b = triangleVertices[ base + ( e + 1 ) % 3 ];
for ( let other = next[ e ]; other !== - 1; other = next[ other ] ) {
if ( neighbors[ other ] !== - 1 ) {
continue;
}
const obase = other - other % 3;
if ( a === triangleVertices[ obase + ( other + 1 ) % 3 ] && b === triangleVertices[ other ] ) {
neighbors[ e ] = obase / 3;
neighbors[ other ] = base / 3;
break;
}
}
}
return neighbors;
}
function generateTangentSpaces( triangleVertices, originalTriangles, triangleFlags, faceDerivatives, neighbors, position, normal, output, cornerCount ) {
// Construct one connected orientation group at a time. A stack preserves
// the reference depth-first order without risking the JS call stack.
const assignedCorners = new Uint8Array( cornerCount );
const stack = new Int32Array( cornerCount / 3 + 1 );
const groupMembers = new Int32Array( cornerCount / 3 );
let projectedDerivatives = new Float32Array( 64 * 6 );
let cornerAngles = new Float32Array( 64 );
let subgroupMembers = new Int32Array( 64 );
let memberAny = new Uint8Array( 64 );
for ( let seed = 0; seed < cornerCount; seed ++ ) {
const seedTriangle = seed / 3 | 0;
if ( assignedCorners[ seed ] || ( triangleFlags[ seedTriangle ] & GROUP_WITH_ANY ) ) {
continue;
}
const representative = triangleVertices[ seed ];
const orientation = triangleFlags[ seedTriangle ] & ORIENTATION_PRESERVING;
let groupSize = 0;
let stackSize = 0;
stack[ stackSize ++ ] = seedTriangle;
while ( stackSize ) {
const f = stack[ -- stackSize ];
const base = f * 3;
const c = triangleVertices[ base ] === representative ? 0 : triangleVertices[ base + 1 ] === representative ? 1 : 2;
const corner = base + c;
if ( assignedCorners[ corner ] ) {
continue;
}
// The first group reaching a UV-degenerate triangle fixes its orientation.
if ( ( triangleFlags[ f ] & GROUP_WITH_ANY ) && ! assignedCorners[ base ] && ! assignedCorners[ base + 1 ] && ! assignedCorners[ base + 2 ] ) {
triangleFlags[ f ] = triangleFlags[ f ] & ~ ORIENTATION_PRESERVING | orientation;
}
if ( ( triangleFlags[ f ] & ORIENTATION_PRESERVING ) !== orientation ) {
continue;
}
assignedCorners[ corner ] = 1;
groupMembers[ groupSize ++ ] = corner;
const left = neighbors[ corner ];
const right = neighbors[ base + ( c + 2 ) % 3 ];
if ( right >= 0 ) {
stack[ stackSize ++ ] = right;
}
if ( left >= 0 ) {
stack[ stackSize ++ ] = left;
}
}
// Evaluation accumulates in ascending face order, as in the reference C.
for ( let i = 1; i < groupSize; i ++ ) {
const corner = groupMembers[ i ];
let j = i - 1;
while ( j >= 0 && groupMembers[ j ] > corner ) {
groupMembers[ j + 1 ] = groupMembers[ j ];
j --;
}
groupMembers[ j + 1 ] = corner;
}
if ( cornerAngles.length < groupSize ) {
cornerAngles = new Float32Array( groupSize );
projectedDerivatives = new Float32Array( groupSize * 6 );
subgroupMembers = new Int32Array( groupSize );
memberAny = new Uint8Array( groupSize );
}
const n = representative * 3;
const nx = normal[ n ];
const ny = normal[ n + 1 ];
const nz = normal[ n + 2 ];
for ( let i = 0; i < groupSize; i ++ ) {
const corner = groupMembers[ i ];
const f = corner / 3 | 0;
const d = f * 6;
const p = i * 6;
// Projections are inlined: V8 boxes the arguments of calls it does not inline.
let sx = faceDerivatives[ d ], sy = faceDerivatives[ d + 1 ], sz = faceDerivatives[ d + 2 ];
const sd = fround( fround( fround( sx * nx ) + fround( sy * ny ) ) + fround( sz * nz ) );
sx = fround( sx - fround( sd * nx ) );
sy = fround( sy - fround( sd * ny ) );
sz = fround( sz - fround( sd * nz ) );
if ( Math.abs( sx ) > FLOAT_MIN || Math.abs( sy ) > FLOAT_MIN || Math.abs( sz ) > FLOAT_MIN ) {
const s = fround( 1 / fround( Math.sqrt( fround( fround( fround( sx * sx ) + fround( sy * sy ) ) + fround( sz * sz ) ) ) ) );
sx *= s;
sy *= s;
sz *= s;
}
projectedDerivatives[ p ] = sx;
projectedDerivatives[ p + 1 ] = sy;
projectedDerivatives[ p + 2 ] = sz;
let tx = faceDerivatives[ d + 3 ], ty = faceDerivatives[ d + 4 ], tz = faceDerivatives[ d + 5 ];
const td = fround( fround( fround( tx * nx ) + fround( ty * ny ) ) + fround( tz * nz ) );
tx = fround( tx - fround( td * nx ) );
ty = fround( ty - fround( td * ny ) );
tz = fround( tz - fround( td * nz ) );
if ( Math.abs( tx ) > FLOAT_MIN || Math.abs( ty ) > FLOAT_MIN || Math.abs( tz ) > FLOAT_MIN ) {
const s = fround( 1 / fround( Math.sqrt( fround( fround( fround( tx * tx ) + fround( ty * ty ) ) + fround( tz * tz ) ) ) ) );
tx *= s;
ty *= s;
tz *= s;
}
projectedDerivatives[ p + 3 ] = tx;
projectedDerivatives[ p + 4 ] = ty;
projectedDerivatives[ p + 5 ] = tz;
memberAny[ i ] = triangleFlags[ f ] & GROUP_WITH_ANY;
if ( memberAny[ i ] ) {
cornerAngles[ i ] = 0;
continue;
}
const c = corner % 3;
const base = corner - c;
const prev = triangleVertices[ base + ( c + 2 ) % 3 ] * 3;
const after = triangleVertices[ base + ( c + 1 ) % 3 ] * 3;
let ax = fround( position[ prev ] - position[ n ] );
let ay = fround( position[ prev + 1 ] - position[ n + 1 ] );
let az = fround( position[ prev + 2 ] - position[ n + 2 ] );
let e = fround( fround( fround( ax * nx ) + fround( ay * ny ) ) + fround( az * nz ) );
ax = fround( ax - fround( e * nx ) );
ay = fround( ay - fround( e * ny ) );
az = fround( az - fround( e * nz ) );
if ( Math.abs( ax ) > FLOAT_MIN || Math.abs( ay ) > FLOAT_MIN || Math.abs( az ) > FLOAT_MIN ) {
const s = fround( 1 / fround( Math.sqrt( fround( fround( fround( ax * ax ) + fround( ay * ay ) ) + fround( az * az ) ) ) ) );
ax = fround( ax * s );
ay = fround( ay * s );
az = fround( az * s );
}
let bx = fround( position[ after ] - position[ n ] );
let by = fround( position[ after + 1 ] - position[ n + 1 ] );
let bz = fround( position[ after + 2 ] - position[ n + 2 ] );
e = fround( fround( fround( bx * nx ) + fround( by * ny ) ) + fround( bz * nz ) );
bx = fround( bx - fround( e * nx ) );
by = fround( by - fround( e * ny ) );
bz = fround( bz - fround( e * nz ) );
if ( Math.abs( bx ) > FLOAT_MIN || Math.abs( by ) > FLOAT_MIN || Math.abs( bz ) > FLOAT_MIN ) {
const s = fround( 1 / fround( Math.sqrt( fround( fround( fround( bx * bx ) + fround( by * by ) ) + fround( bz * bz ) ) ) ) );
bx = fround( bx * s );
by = fround( by * s );
bz = fround( bz * s );
}
const cosine = fround( fround( fround( ax * bx ) + fround( ay * by ) ) + fround( az * bz ) );
cornerAngles[ i ] = Math.acos( Math.max( - 1, Math.min( 1, cosine ) ) );
}
// At the default threshold, a group only splits for opposite projected
// derivatives. Reuse the previous subgroup's result when its members
// match, so the usual unsplit group is evaluated only once.
let previousSubgroupSize = - 1;
let tangentX = 0;
let tangentY = 0;
let tangentZ = 0;
// Usually no pair is opposed and every subgroup is the whole group.
let split = false;
for ( let i = 0; i < groupSize && ! split; i ++ ) {
if ( memberAny[ i ] ) {
continue;
}
const p = i * 6;
for ( let j = i + 1; j < groupSize; j ++ ) {
const q = j * 6;
if ( ! memberAny[ j ] && ! (
fround( fround( fround( projectedDerivatives[ p ] * projectedDerivatives[ q ] ) + fround( projectedDerivatives[ p + 1 ] * projectedDerivatives[ q + 1 ] ) ) + fround( projectedDerivatives[ p + 2 ] * projectedDerivatives[ q + 2 ] ) ) > - 1 &&
fround( fround( fround( projectedDerivatives[ p + 3 ] * projectedDerivatives[ q + 3 ] ) + fround( projectedDerivatives[ p + 4 ] * projectedDerivatives[ q + 4 ] ) ) + fround( projectedDerivatives[ p + 5 ] * projectedDerivatives[ q + 5 ] ) ) > - 1 ) ) {
split = true;
break;
}
}
}
if ( ! split ) {
for ( let j = 0; j < groupSize; j ++ ) {
subgroupMembers[ j ] = j;
}
}
for ( let i = 0; i < groupSize; i ++ ) {
const f = groupMembers[ i ] / 3 | 0;
const p = i * 6;
let subgroupSize = split ? 0 : groupSize;
let sameSubgroup = true;
const anyI = memberAny[ i ];
const sx = projectedDerivatives[ p ], sy = projectedDerivatives[ p + 1 ], sz = projectedDerivatives[ p + 2 ];
const tx = projectedDerivatives[ p + 3 ], ty = projectedDerivatives[ p + 4 ], tz = projectedDerivatives[ p + 5 ];
for ( let j = 0; split && j < groupSize; j ++ ) {
const q = j * 6;
if ( i === j || anyI || memberAny[ j ] || (
fround( fround( fround( sx * projectedDerivatives[ q ] ) + fround( sy * projectedDerivatives[ q + 1 ] ) ) + fround( sz * projectedDerivatives[ q + 2 ] ) ) > - 1 &&
fround( fround( fround( tx * projectedDerivatives[ q + 3 ] ) + fround( ty * projectedDerivatives[ q + 4 ] ) ) + fround( tz * projectedDerivatives[ q + 5 ] ) ) > - 1 ) ) {
if ( subgroupMembers[ subgroupSize ] !== j ) {
sameSubgroup = false;
}
subgroupMembers[ subgroupSize ++ ] = j;
}
}
if ( subgroupSize !== previousSubgroupSize || ! sameSubgroup ) {
let x = 0;
let y = 0;
let z = 0;
for ( let j = 0; j < subgroupSize; j ++ ) {
const index = subgroupMembers[ j ];
// Degenerate derivatives do not contribute, even if they contain NaNs.
if ( memberAny[ index ] ) {
continue;
}
const q = index * 6;
const a = cornerAngles[ index ];
x = fround( x + fround( a * projectedDerivatives[ q ] ) );
y = fround( y + fround( a * projectedDerivatives[ q + 1 ] ) );
z = fround( z + fround( a * projectedDerivatives[ q + 2 ] ) );
}
if ( isNonZero( x, y, z ) ) {
const s = fround( 1 / vectorLength( x, y, z ) );
x = fround( x * s );
y = fround( y * s );
z = fround( z * s );
}
previousSubgroupSize = subgroupSize;
tangentX = x;
tangentY = y;
tangentZ = z;
}
const corner = groupMembers[ i ];
const dst = ( originalTriangles[ f ] * 3 + corner % 3 ) * 4;
output[ dst ] = tangentX;
output[ dst + 1 ] = tangentY;
output[ dst + 2 ] = tangentZ;
output[ dst + 3 ] = orientation ? 1 : - 1;
}
}
}