From 82d89b1bdeed002c7214fb48c8b0e6acb6b19f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guimmara?= <5512096+sguimmara@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:17:17 +0100 Subject: [PATCH] feat(PNTSLoader): support NORMAL and NORMAL_OCT16P semantics (#594) (#900) --- src/three/loaders/PNTSLoader.js | 32 ++++++++++++++++++++-- src/utilities/decodeOctNormal.js | 47 ++++++++++++++++++++++++++++++++ test/decodeOctNormal.test.js | 39 ++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/utilities/decodeOctNormal.js create mode 100644 test/decodeOctNormal.test.js diff --git a/src/three/loaders/PNTSLoader.js b/src/three/loaders/PNTSLoader.js index b9cd13150..d96b58bd5 100644 --- a/src/three/loaders/PNTSLoader.js +++ b/src/three/loaders/PNTSLoader.js @@ -9,6 +9,7 @@ import { Color, } from 'three'; import { rgb565torgb } from '../../utilities/rgb565torgb.js'; +import { decodeOctNormal } from '../../utilities/decodeOctNormal.js'; const DRACO_ATTRIBUTE_MAP = { RGB: 'color', @@ -82,6 +83,8 @@ export class PNTSLoader extends PNTSLoaderBase { // handle non compressed case const POINTS_LENGTH = featureTable.getData( 'POINTS_LENGTH' ); const POSITION = featureTable.getData( 'POSITION', POINTS_LENGTH, 'FLOAT', 'VEC3' ); + const NORMAL = featureTable.getData( 'NORMAL', POINTS_LENGTH, 'FLOAT', 'VEC3' ); + const NORMAL_OCT16P = featureTable.getData( 'NORMAL', POINTS_LENGTH, 'UNSIGNED_BYTE', 'VEC2' ); const RGB = featureTable.getData( 'RGB', POINTS_LENGTH, 'UNSIGNED_BYTE', 'VEC3' ); const RGBA = featureTable.getData( 'RGBA', POINTS_LENGTH, 'UNSIGNED_BYTE', 'VEC4' ); const RGB565 = featureTable.getData( 'RGB565', POINTS_LENGTH, 'UNSIGNED_SHORT', 'SCALAR' ); @@ -116,6 +119,33 @@ export class PNTSLoader extends PNTSLoaderBase { } + if ( NORMAL !== null ) { + + geometry.setAttribute( 'normal', new BufferAttribute( NORMAL, 3, false ) ); + + } else if ( NORMAL_OCT16P !== null ) { + + const decodedNormals = new Float32Array( POINTS_LENGTH * 3 ); + + const n = new Vector3(); + + for ( let i = 0; i < POINTS_LENGTH; i ++ ) { + + const x = NORMAL_OCT16P[ i * 2 ]; + const y = NORMAL_OCT16P[ i * 2 + 1 ]; + + const normal = decodeOctNormal( x, y, n ); + + decodedNormals[ i * 3 ] = normal.x; + decodedNormals[ i * 3 + 1 ] = normal.y; + decodedNormals[ i * 3 + 2 ] = normal.z; + + } + + geometry.setAttribute( 'normal', new BufferAttribute( decodedNormals, 3, false ) ); + + } + if ( RGBA !== null ) { geometry.setAttribute( 'color', new BufferAttribute( RGBA, 4, true ) ); @@ -164,8 +194,6 @@ export class PNTSLoader extends PNTSLoaderBase { [ 'BATCH_LENGTH', - 'NORMAL', - 'NORMAL_OCT16P', ].forEach( ( feature ) => { if ( feature in featureTable.header ) { diff --git a/src/utilities/decodeOctNormal.js b/src/utilities/decodeOctNormal.js new file mode 100644 index 000000000..675091a29 --- /dev/null +++ b/src/utilities/decodeOctNormal.js @@ -0,0 +1,47 @@ +import { Vector2, MathUtils, Vector3 } from 'three'; + +const f = new Vector2(); + +/** + * Decode an octahedron-encoded normal (as a pair of 8-bit unsigned numbers) into a Vector3. + * + * Resources: + * - https://stackoverflow.com/a/74745666/2704779 + * - https://knarkowicz.wordpress.com/2014/04/16/octahedron-normal-vector-encoding/ + * @param {number} x The unsigned 8-bit X coordinate on the projected octahedron. + * @param {number} y The unsigned 8-bit Y coordinate on the projected octahedron. + * @param {Vector3} [target] The target vector. + */ +export function decodeOctNormal( x, y, target = new Vector3() ) { + + f.set( x, y ).divideScalar( 256 ).multiplyScalar( 2 ).subScalar( 1 ); + + target.set( f.x, f.y, 1 - Math.abs( f.x ) - Math.abs( f.y ) ); + + const t = MathUtils.clamp( - target.z, 0, 1 ); + + if ( target.x >= 0 ) { + + target.setX( target.x - t ); + + } else { + + target.setX( target.x + t ); + + } + + if ( target.y >= 0 ) { + + target.setY( target.y - t ); + + } else { + + target.setY( target.y + t ); + + } + + target.normalize(); + + return target; + +} diff --git a/test/decodeOctNormal.test.js b/test/decodeOctNormal.test.js new file mode 100644 index 000000000..bdea3f4ba --- /dev/null +++ b/test/decodeOctNormal.test.js @@ -0,0 +1,39 @@ +import { decodeOctNormal } from '../src/utilities/decodeOctNormal'; + +const PRECISION = 0.005; + +describe( 'decodeOctNormal', () => { + + it.each( [ + // 4 corners of the projected octahedron + [ 0, 0, 0, 0, - 1 ], + [ 255, 255, 0, 0, - 1 ], + [ 0, 255, 0, 0, - 1 ], + [ 255, 0, 0, 0, - 1 ], + + // Center of the projected octahedron + [ 127, 127, 0, 0, 1 ], + + // Midpoint of left edge of the projected octahedron + [ 0, 127, - 1, 0, 0 ], + + // Midpoint of right edge of the projected octahedron + [ 255, 127, 1, 0, 0 ], + + // Midpoint of top edge of the projected octahedron + [ 127, 255, 0, 1, 0 ], + + // Midpoint of bottom edge of the projected octahedron + [ 127, 0, 0, - 1, 0 ], + ] )( 'should decode (%s, %s) into [%s, %s, %s]', ( encX, encY, x, y, z ) => { + + const result = decodeOctNormal( encX, encY ); + + expect( result.length() ).toBeCloseTo( 1, PRECISION ); + expect( result.x ).toBeCloseTo( x, PRECISION ); + expect( result.y ).toBeCloseTo( y, PRECISION ); + expect( result.z ).toBeCloseTo( z, PRECISION ); + + } ); + +} );