Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
contents: read
steps:
- name: Git checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
Expand Down Expand Up @@ -54,7 +54,7 @@ jobs:
CI: ${{ matrix.CI }}
steps:
- name: Git checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-code-scanning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/read-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
contents: read
steps:
- name: Git checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/report-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:

# This runs on the base branch of the PR, meaning "dev"
- name: Git checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
Expand Down
2 changes: 2 additions & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@
"webgpu_video_panorama",
"webgpu_volume_caustics",
"webgpu_volume_cloud",
"webgpu_volume_fire",
"webgpu_volume_lighting",
"webgpu_volume_lighting_rectarea",
"webgpu_volume_lighting_traa",
Expand Down Expand Up @@ -582,6 +583,7 @@
"misc_exporter_draco",
"misc_exporter_gcode",
"misc_exporter_gltf",
"misc_exporter_gltf_normals",
"misc_exporter_obj",
"misc_exporter_ply",
"misc_exporter_stl",
Expand Down
95 changes: 86 additions & 9 deletions examples/jsm/exporters/GLTFExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,57 @@ class GLTFWriter {
}


/**
* Builds a copy of the given normal map with the red and/or green channels
* inverted (`color = 255 - color`). This is used to bake the sign of
* `material.normalScale` and the tangent-space convention into the texture,
* since glTF only supports OpenGL-style normal maps with a univariate,
* positive scale.
*
* @param {THREE.Texture} normalMap The source normal map.
* @param {boolean} flipX Whether to invert the red channel (normal X).
* @param {boolean} flipY Whether to invert the green channel (normal Y).
* @return {Promise<THREE.Texture>} The derived normal map texture.
*/
async buildNormalMapTextureAsync( normalMap, flipX, flipY ) {

if ( normalMap instanceof CompressedTexture ) {

normalMap = await this.decompressTextureAsync( normalMap );

}

const image = normalMap.image;

const canvas = getCanvas();
canvas.width = image.width;
canvas.height = image.height;

const context = canvas.getContext( '2d', {
willReadFrequently: true,
} );

context.drawImage( image, 0, 0, canvas.width, canvas.height );

const imageData = context.getImageData( 0, 0, canvas.width, canvas.height );
const data = imageData.data;

for ( let i = 0; i < data.length; i += 4 ) {

if ( flipX ) data[ i + 0 ] = 255 - data[ i + 0 ];
if ( flipY ) data[ i + 1 ] = 255 - data[ i + 1 ];

}

context.putImageData( imageData, 0, 0 );

const texture = normalMap.clone();
texture.source = new Source( canvas );

return texture;

}

async decompressTextureAsync( texture, maxTextureSize = Infinity ) {

if ( this.textureUtils === null ) {
Expand Down Expand Up @@ -1575,14 +1626,20 @@ class GLTFWriter {
/**
* Process material
* @param {THREE.Material} material Material to process
* @param {THREE.BufferGeometry} [geometry] Geometry the material is used with.
* @return {Promise<?number>} Index of the processed material in the "materials" array
*/
async processMaterialAsync( material ) {
async processMaterialAsync( material, geometry ) {

const cache = this.cache;
const json = this.json;

if ( cache.materials.has( material ) ) return cache.materials.get( material );
// Whether the geometry provides explicit tangents. The exported normal map depends on
// this, so it is part of the material cache key.
const hasTangent = geometry !== undefined && geometry.hasAttribute( 'tangent' );
const cacheKey = material.normalMap ? material.uuid + ':' + hasTangent : material.uuid;

if ( cache.materials.has( cacheKey ) ) return cache.materials.get( cacheKey );

if ( material.isShaderMaterial ) {

Expand Down Expand Up @@ -1677,16 +1734,36 @@ class GLTFWriter {
// normalTexture
if ( material.normalMap ) {

const normalScale = material.normalScale;

// glTF only supports OpenGL-style normal maps with a univariate, positive scale.
// A negative `normalScale` component is baked into the texture by inverting the
// corresponding channel. Meshes without explicit tangents use the opposite
// green-channel convention, so the green channel is inverted in that case too.
//
// The no-tangent green flip is the counterpart of GLTFLoader, which negates
// `normalScale.y` on import for the same case.
const flipX = normalScale.x < 0;
const flipY = hasTangent ? normalScale.y < 0 : normalScale.y > 0;

let normalMap = material.normalMap;

if ( flipX || flipY ) {

normalMap = await this.buildNormalMapTextureAsync( material.normalMap, flipX, flipY );

}

const normalMapDef = {
index: await this.processTextureAsync( material.normalMap ),
index: await this.processTextureAsync( normalMap ),
texCoord: material.normalMap.channel
};

if ( material.normalScale && material.normalScale.x !== 1 ) {
if ( Math.abs( normalScale.x ) !== 1 ) {

// glTF normal scale is univariate. Ignore `y`, which may be flipped.
// Context: https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
normalMapDef.scale = material.normalScale.x;
// glTF normal scale is univariate. The magnitude of `x` is used; the sign of
// both components has already been baked into the texture above.
normalMapDef.scale = Math.abs( normalScale.x );

}

Expand Down Expand Up @@ -1743,7 +1820,7 @@ class GLTFWriter {
} );

const index = json.materials.push( materialDef ) - 1;
cache.materials.set( material, index );
cache.materials.set( cacheKey, index );
return index;

}
Expand Down Expand Up @@ -2061,7 +2138,7 @@ class GLTFWriter {

}

const material = await this.processMaterialAsync( materials[ groups[ i ].materialIndex ] );
const material = await this.processMaterialAsync( materials[ groups[ i ].materialIndex ], geometry );

if ( material !== null ) primitive.material = material;

Expand Down
107 changes: 107 additions & 0 deletions examples/jsm/tsl/math/curlNoise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { mod, Fn, vec2, div, vec4, dot, floor, step, sub, min, max, mul, abs, vec3, inverseSqrt, add, float } from 'three/tsl';

/**
* Permutation polynomial for noise generation.
*
* @tsl
* @function
* @param {Node<vec4>} x - Input vector.
* @return {Node<vec4>} Permuted vector.
*/
export const permute = /*@__PURE__*/ Fn( ( [ x ] ) => {

return mod( x.mul( x ).mul( 34. ).add( x ), 289. );

}, { x: 'vec4', return: 'vec4' } );

/**
* 3D Simplex noise implementation in TSL.
*
* @tsl
* @function
* @param {Node<vec3>} v - Input coordinate vector.
* @return {Node<float>} Simplex noise value.
*/
export const snoise = /*@__PURE__*/ Fn( ( [ v ] ) => {

const C = div( 1., vec2( 6, 3 ) );
const D = vec4( 0, .5, 1, 2 );
const i = floor( v.add( dot( v, C.yyy ) ) );
const x0 = v.sub( i ).add( dot( i, C.xxx ) );
const g = step( x0.yzx, x0.xyz );
const l = sub( 1., g );
const i1 = min( g.xyz, l.zxy );
const i2 = max( g.xyz, l.zxy );
const x1 = x0.sub( i1 ).add( C.x );
const x2 = x0.sub( i2 ).add( C.y );
const x3 = x0.sub( D.yyy );
i.assign( mod( i, 289. ) );
const p = permute( permute( permute( i.z.add( vec4( 0., i1.z, i2.z, 1. ) ) ).add( i.y ).add( vec4( 0., i1.y, i2.y, 1. ) ) ).add( i.x ).add( vec4( 0., i1.x, i2.x, 1. ) ) );
const ns = mul( .142857142857, D.wyz ).sub( D.xzx );
const j = p.sub( mul( 49., floor( p.mul( ns.z ).mul( ns.z ) ) ) );
const x_ = floor( j.mul( ns.z ) );
const x = x_.mul( ns.x ).add( ns.yyyy );
const y = floor( j.sub( mul( 7., x_ ) ) ).mul( ns.x ).add( ns.yyyy );
const h = sub( 1., abs( x ) ).sub( abs( y ) );
const b0 = vec4( x.xy, y.xy );
const b1 = vec4( x.zw, y.zw );
const sh = step( h, vec4( 0 ) ).negate();
const a0 = b0.xzyw.add( floor( b0 ).mul( 2. ).add( 1. ).xzyw.mul( sh.xxyy ) );
const a1 = b1.xzyw.add( floor( b1 ).mul( 2. ).add( 1. ).xzyw.mul( sh.zzww ) );
const p0 = vec3( a0.xy, h.x );
const p1 = vec3( a0.zw, h.y );
const p2 = vec3( a1.xy, h.z );
const p3 = vec3( a1.zw, h.w );
const norm = inverseSqrt( vec4( dot( p0, p0 ), dot( p1, p1 ), dot( p2, p2 ), dot( p3, p3 ) ) );
p0.mulAssign( norm.x );
p1.mulAssign( norm.y );
p2.mulAssign( norm.z );
p3.mulAssign( norm.w );
const m = max( sub( .6, vec4( dot( x0, x0 ), dot( x1, x1 ), dot( x2, x2 ), dot( x3, x3 ) ) ), 0. );

return add( .5, mul( 12., dot( m.mul( m ).mul( m ), vec4( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 ), dot( p3, x3 ) ) ) ) );

}, { v: 'vec3', return: 'float' } );

/**
* 3D Simplex noise vector. Returns a vec3 containing three independent noise samples.
*
* @tsl
* @function
* @param {Node<vec3>} x - Input coordinate vector.
* @return {Node<vec3>} Vector of three noise values.
*/
export const snoiseVec3 = /*@__PURE__*/ Fn( ( [ x ] ) => {

return vec3( snoise( vec3( x ).mul( 2. ).sub( 1. ) ), snoise( vec3( x.y.sub( 19.1 ), x.z.add( 33.4 ), x.x.add( 47.2 ) ) ).mul( 2. ).sub( 1. ), snoise( vec3( x.z.add( 74.2 ), x.x.sub( 124.5 ), x.y.add( 99.4 ) ).mul( 2. ).sub( 1. ) ) );

}, { x: 'vec3', return: 'vec3' } );

/**
* 3D Curl noise in TSL. Generates a divergence-free vector field from simplex noise.
*
* @tsl
* @function
* @param {Node<vec3>} p - Input coordinate vector.
* @return {Node<vec3>} Curl noise vector.
*/
export const curlNoise = /*@__PURE__*/ Fn( ( [ p ] ) => {

const e = float( .1 );
const dx = vec3( e, 0.0, 0.0 );
const dy = vec3( 0.0, e, 0.0 );
const dz = vec3( 0.0, 0.0, e );
const p_x0 = snoiseVec3( p.sub( dx ) );
const p_x1 = snoiseVec3( p.add( dx ) );
const p_y0 = snoiseVec3( p.sub( dy ) );
const p_y1 = snoiseVec3( p.add( dy ) );
const p_z0 = snoiseVec3( p.sub( dz ) );
const p_z1 = snoiseVec3( p.add( dz ) );
const x = p_y1.z.sub( p_y0.z ).sub( p_z1.y ).add( p_z0.y );
const y = p_z1.x.sub( p_z0.x ).sub( p_x1.z ).add( p_x0.z );
const z = p_x1.y.sub( p_x0.y ).sub( p_y1.x ).add( p_y0.x );
const divisor = div( 1.0, mul( 2.0, e ) );

return vec3( x, y, z ).mul( divisor );

}, { p: 'vec3', return: 'vec3' } );
Loading
Loading