UNPKG

@needle-tools/materialx

Version:

MaterialX material support for three.js and Needle Engine – render physically based MaterialX shaders in the browser via WebAssembly

179 lines (152 loc) 7.15 kB
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; }