diff --git a/src/constants.js b/src/constants.js index dcf6d58e955bef..df88d7c8d29acf 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1280,6 +1280,30 @@ export const LinearTransfer = 'linear'; */ export const SRGBTransfer = 'srgb'; +/** + * No normal map packing. + * + * @type {string} + * @constant + */ +export const NoNormalPacking = ''; + +/** + * Normal RG packing. + * + * @type {string} + * @constant + */ +export const NormalRGPacking = 'rg'; + +/** + * Normal GA packing. + * + * @type {string} + * @constant + */ +export const NormalGAPacking = 'ga'; + /** * Sets the stencil buffer value to `0`. * diff --git a/src/nodes/accessors/MaterialNode.js b/src/nodes/accessors/MaterialNode.js index 5267ef2141a604..cc3479eef75b21 100644 --- a/src/nodes/accessors/MaterialNode.js +++ b/src/nodes/accessors/MaterialNode.js @@ -235,6 +235,12 @@ class MaterialNode extends Node { node = normalMap( this.getTexture( 'normal' ), this.getCache( 'normalScale', 'vec2' ) ); node.normalMapType = material.normalMapType; + if ( material.normalMap.userData && material.normalMap.userData.unpackNormalMode ) { + + node.unpackNormalMode = material.normalMap.userData.unpackNormalMode; + + } + } else if ( material.bumpMap ) { node = bumpMap( this.getTexture( 'bump' ).r, this.getFloat( 'bumpScale' ) ); diff --git a/src/nodes/display/NormalMapNode.js b/src/nodes/display/NormalMapNode.js index 06403c19dbdc93..c86e1fbe1208c7 100644 --- a/src/nodes/display/NormalMapNode.js +++ b/src/nodes/display/NormalMapNode.js @@ -4,8 +4,9 @@ import { normalView, transformNormalToView } from '../accessors/Normal.js'; import { TBNViewMatrix } from '../accessors/AccessorsUtils.js'; import { nodeProxy, vec3 } from '../tsl/TSLBase.js'; -import { TangentSpaceNormalMap, ObjectSpaceNormalMap } from '../../constants.js'; +import { TangentSpaceNormalMap, ObjectSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking } from '../../constants.js'; import { directionToFaceDirection } from './FrontFacingNode.js'; +import { unpackNormal } from '../utils/Packing.js'; import { error } from '../../utils.js'; /** @@ -58,14 +59,48 @@ class NormalMapNode extends TempNode { */ this.normalMapType = TangentSpaceNormalMap; + /** + * Controls how to unpack the sampled normal map values. + * + * @type {string} + * @default NoNormalPacking + */ + this.unpackNormalMode = NoNormalPacking; + } setup( { material } ) { - const { normalMapType, scaleNode } = this; + const { normalMapType, scaleNode, unpackNormalMode } = this; let normalMap = this.node.mul( 2.0 ).sub( 1.0 ); + if ( normalMapType === TangentSpaceNormalMap ) { + + if ( unpackNormalMode == NormalRGPacking ) { + + normalMap = unpackNormal( normalMap.xy ); + + } else if ( unpackNormalMode == NormalGAPacking ) { + + normalMap = unpackNormal( normalMap.yw ); + + } else if ( unpackNormalMode != NoNormalPacking ) { + + console.error( `THREE.NodeMaterial: Unexpected unpack normal mode: ${ unpackNormalMode }` ); + + } + + } else { + + if ( unpackNormalMode != NoNormalPacking ) { + + console.error( `THREE.NodeMaterial: Normal map type '${ normalMapType }' is not compatible with unpack normal mode '${ unpackNormalMode }'` ); + + } + + } + if ( scaleNode !== null ) { let scale = scaleNode; diff --git a/src/nodes/utils/Packing.js b/src/nodes/utils/Packing.js index e89585494c853a..dba3248bd2d842 100644 --- a/src/nodes/utils/Packing.js +++ b/src/nodes/utils/Packing.js @@ -1,4 +1,5 @@ -import { nodeObject } from '../tsl/TSLBase.js'; +import { nodeObject, vec3, float } from '../tsl/TSLBase.js'; +import { dot, sqrt, saturate } from '../math/MathNode.js'; /** * Packs a direction vector into a color value. @@ -19,3 +20,14 @@ export const directionToColor = ( node ) => nodeObject( node ).mul( 0.5 ).add( 0 * @return {Node} The direction. */ export const colorToDirection = ( node ) => nodeObject( node ).mul( 2.0 ).sub( 1 ); + +/** + * Unpacks a tangent space normal, reconstructing the Z component by projecting the X,Y coordinates onto the hemisphere. + * The X,Y coordinates are expected to be in the [-1, 1] range. + * + * @tsl + * @function + * @param {Node} xy - The X,Y coordinates of the normal. + * @return {Node} The resulting normal. + */ +export const unpackNormal = ( xy ) => vec3( xy, sqrt( saturate( float( 1.0 ).sub( dot( xy, xy ) ) ) ) );