@needle-tools/materialx
Version:
Web runtime support to load and display MaterialX materials in Needle Engine and three.js via the MaterialX WebAssembly library. glTF files containing the `NEEDLE_materials_mtlx` extension can be loaded with this package. There is also experimental suppor
179 lines (152 loc) • 7.15 kB
JavaScript
import { WebGLRenderer, Scene, WebGLRenderTarget, PlaneGeometry, OrthographicCamera, ShaderMaterial, RGBAFormat, FloatType, LinearFilter, Mesh, EquirectangularReflectionMapping, RepeatWrapping, LinearMipMapLinearFilter, Texture } from 'three';
import { getParam } from './utils.js';
const debug = getParam("debugmaterialx");
export const whiteTexture = new Texture();
whiteTexture.needsUpdate = true;
whiteTexture.image = new Image();
whiteTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
/**
* Renders a PMREM environment map to an equirectangular texture with specified roughness
* @param {WebGLRenderer} renderer - Three.js WebGL renderer
* @param {Texture} pmremTexture - PMREM texture (2D CubeUV layout) to convert
* @param {number} [roughness=0.0] - Roughness value (0.0 to 1.0)
* @param {number} [width=1024] - Output texture width
* @param {number} [height=512] - Output texture height
* @param {number} [renderTargetHeight] - Original render target height (optional, for proper PMREM parameter calculation)
* @returns {WebGLRenderTarget} Render target containing the equirectangular texture
* @example
* // Creating an equirectangular texture from a PMREM environment map at a certain roughness level:
* const pmremRenderTarget = pmremGenerator.fromEquirectangular(envMap);
* const equirectRenderTarget = await renderPMREMToEquirect(renderer, pmremRenderTarget.texture, 0.5, 2048, 1024, pmremRenderTarget.height);
*
* // Use the rendered equirectangular texture
* const equirectTexture = equirectRenderTarget.texture;
*
* // Apply to your material or save/export
* someMaterial.map = equirectTexture;
*
* // Don't forget to dispose when done
* // equirectRenderTarget.dispose();
*/
export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, width = 1024, height = 512, renderTargetHeight) {
// TODO Validate inputs
// console.log(renderer, pmremTexture);
// Calculate PMREM parameters
// For PMREM CubeUV layout, we need the cube face size to calculate proper parameters
// Use renderTargetHeight if provided, otherwise try to derive from texture
let imageHeight;
if (renderTargetHeight) {
imageHeight = renderTargetHeight;
} else if (pmremTexture.image) {
imageHeight = pmremTexture.image.height / 4; // Fallback: assume CubeUV layout height / 4
} else {
imageHeight = 256; // Final fallback
}
const maxMip = Math.log2(imageHeight) - 2;
const cubeUVHeight = imageHeight;
const cubeUVWidth = 3 * Math.max(Math.pow(2, maxMip), 7 * 16);
// Create render target for equirectangular output
const renderTarget = new WebGLRenderTarget(width, height, {
format: RGBAFormat,
type: FloatType,
minFilter: LinearMipMapLinearFilter,
magFilter: LinearFilter,
generateMipmaps: true,
wrapS: RepeatWrapping,
anisotropy: renderer.capabilities.getMaxAnisotropy(),
});
// Create fullscreen quad geometry and camera
const geometry = new PlaneGeometry(2, 2);
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
// Create shader material for PMREM to equirectangular conversion
const material = new ShaderMaterial({
defines: {
USE_ENVMAP: '',
ENVMAP_TYPE_CUBE_UV: '',
CUBEUV_TEXEL_WIDTH: 1.0 / cubeUVWidth,
CUBEUV_TEXEL_HEIGHT: 1.0 / cubeUVHeight,
CUBEUV_MAX_MIP: (maxMip + 0) + '.0',
},
uniforms: {
envMap: { value: pmremTexture },
roughness: { value: roughness }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`,
fragmentShader: `
uniform sampler2D envMap;
uniform float roughness;
varying vec2 vUv;
#include <common>
#include <cube_uv_reflection_fragment>
void main() {
// Convert UV coordinates to equirectangular direction
vec2 uv = vUv;
// Map UV (0,1) to spherical coordinates
// Longitude: -π to π, Latitude: 0 to π
float phi = uv.x * 2.0 * PI - PI; // Longitude (-π to π)
float theta = uv.y * PI; // Latitude (0 to π)
// Rotate 90° around Y
phi -= PI / 2.0; // Adjust to match Three.js convention
// Convert spherical to cartesian coordinates
vec3 direction = vec3(
sin(theta) * cos(phi), // x
cos(theta), // y
sin(theta) * sin(phi) // z
);
// Sample the PMREM cube texture using the direction and roughness
#ifdef ENVMAP_TYPE_CUBE_UV
vec4 envColor = textureCubeUV(envMap, direction, roughness);
#else
vec4 envColor = vec4(1.0, 0.0, 1.0, 1.0); // Magenta fallback
#endif
gl_FragColor = vec4(envColor.rgb, 1.0);
}
`
});
// Create temporary scene and mesh for rendering
const tempScene = new Scene();
const mesh = new Mesh(geometry, material);
tempScene.add(mesh);
// Store current renderer state
const currentRenderTarget = renderer.getRenderTarget();
const currentAutoClear = renderer.autoClear;
const currentXrEnabled = renderer.xr.enabled;
const currentShadowMapEnabled = renderer.shadowMap.enabled;
renderTarget.texture.generateMipmaps = true;
try {
// Disable XR and shadow mapping during our render to avoid interference
renderer.xr.enabled = false;
renderer.shadowMap.enabled = false;
// Render to our target
renderer.autoClear = true;
renderer.setRenderTarget(renderTarget);
renderer.clear(); // Explicitly clear the render target
renderer.render(tempScene, camera);
} finally {
// Restore renderer state completely
renderer.setRenderTarget(currentRenderTarget);
renderer.autoClear = currentAutoClear;
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.enabled = currentShadowMapEnabled;
// Clean up temporary objects
geometry.dispose();
material.dispose();
tempScene.remove(mesh);
}
renderTarget.texture.name = 'PMREM_Equirectangular_Texture_' + roughness.toFixed(2);
renderTarget.texture.mapping = EquirectangularReflectionMapping;
// Log mipmap infos
if (debug) console.log('[MaterialX] PMREM to Equirect Render Target:', {
width: renderTarget.width,
height: renderTarget.height,
mipmaps: renderTarget.texture.mipmaps?.length,
roughness: roughness,
});
return renderTarget;
}