diff --git a/javascript/MaterialXView/source/helper.js b/javascript/MaterialXView/source/helper.js
index 417512f666..a50153e7d3 100644
--- a/javascript/MaterialXView/source/helper.js
+++ b/javascript/MaterialXView/source/helper.js
@@ -21,7 +21,9 @@ export function prepareEnvTexture(texture, capabilities)
{
let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
newTexture.wrapS = THREE.RepeatWrapping;
- newTexture.anisotropy = capabilities.getMaxAnisotropy();
+ // WebGLRenderer exposes getMaxAnisotropy(); guard for WebGPURenderer capabilities.
+ newTexture.anisotropy = (capabilities && typeof capabilities.getMaxAnisotropy === 'function')
+ ? capabilities.getMaxAnisotropy() : 1;
newTexture.minFilter = THREE.LinearMipmapLinearFilter;
newTexture.magFilter = THREE.LinearFilter;
newTexture.generateMipmaps = true;
@@ -270,7 +272,9 @@ export function getLightRotation()
export function findLights(doc)
{
let lights = [];
- for (let node of doc.getNodes())
+ const nodes = doc.getNodes();
+ if (!nodes) return lights;
+ for (let node of nodes)
{
if (node.getType() === "lightshader")
lights.push(node);
@@ -292,6 +296,7 @@ export function registerLights(mx, lights, genContext)
const lightTypesBound = {};
const lightData = [];
let lightId = 1;
+ if (!lights) return lightData;
for (let light of lights)
{
let nodeDef = light.getNodeDef();
diff --git a/javascript/MaterialXView/source/index.js b/javascript/MaterialXView/source/index.js
index 2840261a40..9998993147 100644
--- a/javascript/MaterialXView/source/index.js
+++ b/javascript/MaterialXView/source/index.js
@@ -8,7 +8,15 @@ import { Viewer } from './viewer.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { dropHandler, dragOverHandler, setLoadingCallback, setSceneLoadingCallback } from './dropHandling.js';
+// Rendering backend, selected at build time per bundle (see webpack.config.js). The WebGL
+// bundle uses classic THREE.WebGLRenderer + RawShaderMaterial (ESSL); the WebGPU bundle
+// aliases `three` to three/webgpu and uses WebGPURenderer + NodeMaterial (WGSL via the
+// upstream WgslShaderGenerator). A toggle switches between the two HTML pages.
+const BACKEND = (typeof __BACKEND__ !== 'undefined') ? __BACKEND__ : 'webgl';
+let TSL = null; // three/tsl namespace, loaded for the WebGPU backend only
+
let renderer, orbitControls;
+let webgpuSupported = null; // cached result of navigator.gpu probe
// FPS overlay state
let fpsOverlay = null;
@@ -50,8 +58,32 @@ function setFPSOverlayVisible(visible) {
createFPSOverlay();
setFPSOverlayVisible(showFPS);
-init();
-viewer.getEditor().updateProperties(0.9);
+probeWebGPU().then(() =>
+{
+ init();
+ viewer.getEditor().updateProperties(0.9);
+});
+
+/** Probe WebGPU availability once; used by the backend toggle and init fallback. */
+async function probeWebGPU()
+{
+ if (webgpuSupported !== null) return webgpuSupported;
+ if (!navigator.gpu)
+ {
+ webgpuSupported = false;
+ return false;
+ }
+ try
+ {
+ const adapter = await navigator.gpu.requestAdapter();
+ webgpuSupported = !!adapter;
+ }
+ catch
+ {
+ webgpuSupported = false;
+ }
+ return webgpuSupported;
+}
// Capture the current frame and save an image file.
function captureFrame()
@@ -94,11 +126,26 @@ function init()
// Set up scene
scene.initialize();
- // Set up renderer
- renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
+ // Set up renderer for the selected backend.
+ if (BACKEND === 'webgpu')
+ {
+ if (!webgpuSupported)
+ {
+ showWebGPUFallbackBanner();
+ return;
+ }
+ renderer = new THREE.WebGPURenderer({ antialias: true, canvas });
+ }
+ else
+ {
+ renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
+ renderer.debug.checkShaderErrors = false;
+ }
renderer.setSize(window.innerWidth, window.innerHeight);
+ // WebGL encodes sRGB in the pixel shader; WebGPU outputs linear and relies on this conversion.
renderer.outputColorSpace = THREE.SRGBColorSpace;
- renderer.debug.checkShaderErrors = false;
+
+ addBackendToggle(webgpuSupported);
window.addEventListener('resize', onWindowResize);
@@ -134,8 +181,17 @@ function init()
.then(({ default: MaterialX }) => MaterialX())
]).then(async ([radianceTexture, irradianceTexture, lightRigXml, mxIn]) =>
{
+ // WebGPU: load the TSL namespace (used by the WGSL→NodeMaterial bridge) and
+ // initialize the device before the first render.
+ if (BACKEND === 'webgpu')
+ {
+ TSL = await import('three/tsl');
+ await renderer.init();
+ }
+
// Initialize viewer + lighting
- await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml);
+ await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml,
+ { backend: BACKEND, TSL });
// Load geometry
let scene = viewer.getScene();
@@ -154,7 +210,13 @@ function init()
animate();
}).catch(err =>
{
- console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err);
+ const mx = viewer.getMx();
+ const message = (Number.isInteger(err) && mx)
+ ? mx.getExceptionMessage(err)
+ : (err && err.message ? err.message : err);
+ console.error(message, err);
+ if (BACKEND === 'webgpu')
+ showWebGPUFallbackBanner('WebGPU initialization failed. Use the WebGL view instead.');
})
// allow dropping files and directories
@@ -187,6 +249,67 @@ function onWindowResize()
renderer.setSize(window.innerWidth, window.innerHeight);
}
+// Floating toggle to switch rendering backend. Each backend is a separate bundle/page
+// (different three build), so switching navigates between index.html and index-webgpu.html
+// while preserving the current ?file/?geom query so the comparison stays on the same content.
+function addBackendToggle(gpuAvailable)
+{
+ const search = window.location.search;
+ const wrap = document.createElement('div');
+ // Bottom-center: clear of the material/geometry selectors (top-left), the property
+ // editor (top-right), and the FPS overlay (bottom-left).
+ wrap.style.cssText = 'position:fixed;bottom:10px;left:50%;transform:translateX(-50%);z-index:1000;' +
+ 'display:flex;gap:4px;flex-wrap:wrap;align-items:center;' +
+ 'font-family:sans-serif;font-size:12px;background:rgba(0,0,0,0.5);padding:4px 6px;border-radius:4px;max-width:90vw;';
+ const label = document.createElement('span');
+ label.textContent = 'Renderer:';
+ label.style.cssText = 'color:#ccc;align-self:center;';
+ wrap.appendChild(label);
+
+ for (const [name, page] of [['WebGL', 'index.html'], ['WebGPU', 'index-webgpu.html']])
+ {
+ const isWebGPU = (name === 'WebGPU');
+ const active = (name.toLowerCase() === BACKEND);
+ const disabled = isWebGPU && gpuAvailable === false;
+ const btn = document.createElement('button');
+ btn.textContent = name;
+ btn.title = disabled
+ ? 'WebGPU is not available in this browser'
+ : (isWebGPU
+ ? 'WebGPU uses MaterialX WGSL + TSL NodeMaterial (same light rig and IBL as WebGL)'
+ : 'WebGL uses full MaterialX light rig + GLSL ES shaders');
+ btn.style.cssText = 'border:none;border-radius:3px;padding:3px 8px;' +
+ (active ? 'background:#4a9;color:#fff;font-weight:bold;' :
+ disabled ? 'background:#222;color:#666;cursor:not-allowed;' :
+ 'background:#333;color:#bbb;cursor:pointer;');
+ if (!active && !disabled)
+ {
+ btn.addEventListener('click', () =>
+ {
+ btn.textContent = '…';
+ btn.disabled = true;
+ window.location.href = page + search;
+ });
+ }
+ wrap.appendChild(btn);
+ }
+ document.body.appendChild(wrap);
+}
+
+function showWebGPUFallbackBanner(message)
+{
+ const search = window.location.search;
+ const banner = document.createElement('div');
+ banner.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;' +
+ 'background:rgba(0,0,0,0.85);color:#eee;font-family:sans-serif;font-size:14px;z-index:2000;padding:24px;text-align:center;';
+ const link = 'index.html' + search;
+ banner.innerHTML = '
' +
+ (message || 'WebGPU is not available in this browser.') +
+ '
Open WebGL view ';
+ document.body.appendChild(banner);
+ addBackendToggle(false);
+}
+
function animate()
{
requestAnimationFrame(animate);
diff --git a/javascript/MaterialXView/source/mxtsladapter.js b/javascript/MaterialXView/source/mxtsladapter.js
new file mode 100644
index 0000000000..c2cc08b570
--- /dev/null
+++ b/javascript/MaterialXView/source/mxtsladapter.js
@@ -0,0 +1,1190 @@
+/**
+ * mxtsladapter
+ *
+ * Adapts MaterialX-generated WGSL into Three.js TSL / WebGPU.
+ *
+ * The MaterialX `WgslShaderGenerator` emits a complete, standard WGSL module:
+ * helper structs + functions, a uniform block + texture/sampler bindings declared
+ * with `@group(N) @binding(M)`, and a single material entry function that returns
+ * the surface result. Three.js never ingests such a module directly — its only raw
+ * WGSL entry point is `wgslFn`, which accepts exactly one function whose resources
+ * arrive as *parameters* (TSL owns the bindings).
+ *
+ * This module performs that reshaping ("TSL-portable WGSL"):
+ * - `convertToTslPortable(wgsl, manifest)` rewrites the entry function so that the
+ * uniform-block members and texture/sampler bindings become explicit parameters,
+ * and strips the now-unused `@group/@binding` declarations and uniform struct.
+ * Everything else (helper structs/functions) is returned verbatim as `includes`.
+ * - `createMxWgslMaterial({ THREE, TSL, ... })` wires the converted entry up
+ * with `wgslFn`, binding each parameter to the matching TSL node
+ * (`uniform`/`texture`/`sampler`/builtin), and returns a NodeMaterial.
+ *
+ * The manifest is emitted alongside the WGSL by the generator and makes the
+ * conversion deterministic (no fragile parsing of binding layouts).
+ */
+
+// ── WGSL text utilities ─────────────────────────────────────────────────────
+
+function skipWs( src, i ) {
+
+ while ( i < src.length && /\s/.test( src[ i ] ) ) i ++;
+ return i;
+
+}
+
+// Escape a string for safe use as a literal inside a RegExp. The function/resource names
+// here are generator-emitted WGSL identifiers, but escaping keeps the matching correct if
+// that ever changes and documents the intent.
+function escapeRegExp( s ) {
+
+ return s.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
+
+}
+
+const WGSL_TYPE_ALIGN = {
+ i32: 4, u32: 4, f32: 4, bool: 4,
+ vec2f: 8, 'vec2': 8,
+ vec3f: 16, 'vec3': 16,
+ vec4f: 16, 'vec4': 16
+};
+
+const WGSL_TYPE_SIZE = {
+ i32: 4, u32: 4, f32: 4, bool: 4,
+ vec2f: 8, 'vec2': 8,
+ vec3f: 12, 'vec3': 12,
+ vec4f: 16, 'vec4': 16
+};
+
+/** WGSL uniform struct layout (WebGPU host-shareable rules). */
+function layoutUniformStruct( fields ) {
+
+ let offset = 0;
+ let maxAlign = 4;
+ const placements = [];
+ for ( const field of fields ) {
+
+ const align = WGSL_TYPE_ALIGN[ field.type ] || 4;
+ const size = WGSL_TYPE_SIZE[ field.type ] || 4;
+ offset = Math.ceil( offset / align ) * align;
+ placements.push( { name: field.name, type: field.type, offset } );
+ offset += size;
+ maxAlign = Math.max( maxAlign, align );
+
+ }
+ const structSize = Math.ceil( offset / maxAlign ) * maxAlign;
+ return { placements, structSize, vec4Stride: structSize / 16 };
+
+}
+
+/** Parse `array` from a manifest uniform type string. */
+function parseStructArrayType( typeStr, wgslSource, numLights, lightData, layout = null ) {
+
+ const type = typeStr || '';
+ const flatVec4 = type.match( /array/ );
+ if ( flatVec4 ) {
+
+ const flatCount = parseInt( flatVec4[ 1 ], 10 );
+ const stride = layout?.vec4Stride || 1;
+ return {
+ structName: 'LightData',
+ count: Math.ceil( flatCount / stride ),
+ vec4Stride: stride,
+ flatCount
+ };
+
+ }
+ const numeric = type.match( /array<(\w+),\s*(\d+)>/ );
+ if ( numeric ) {
+
+ const count = parseInt( numeric[ 2 ], 10 );
+ return {
+ structName: numeric[ 1 ],
+ count,
+ vec4Stride: layout?.vec4Stride || 1,
+ flatCount: layout ? count * layout.vec4Stride : count
+ };
+
+ }
+ const symbolic = type.match( /array<(\w+),\s*(\w+)>/ );
+ const structName = symbolic ? symbolic[ 1 ] : 'LightData';
+ let count = Math.max( numLights || 0, lightData?.length || 0, 1 );
+ if ( symbolic && wgslSource ) {
+
+ const constRe = new RegExp( `const\\s+${ escapeRegExp( symbolic[ 2 ] ) }:\\s*i32\\s*=\\s*(\\d+)` );
+ const m = wgslSource.match( constRe );
+ if ( m ) count = parseInt( m[ 1 ], 10 );
+
+ }
+ const vec4Stride = layout?.vec4Stride || 1;
+ return { structName, count, vec4Stride, flatCount: count * vec4Stride };
+
+}
+
+/** Parse `struct LightData { ... }` from generated WGSL. */
+export function parseLightDataStruct( wgsl ) {
+
+ const m = wgsl.match( /struct\s+LightData\s*\{([^}]+)\}/ );
+ if ( ! m ) return null;
+ const fields = [];
+ for ( const line of m[ 1 ].split( '\n' ) ) {
+
+ const trimmed = line.trim().replace( /,$/, '' );
+ if ( ! trimmed ) continue;
+ const colon = trimmed.indexOf( ':' );
+ if ( colon < 0 ) continue;
+ fields.push( {
+ name: trimmed.slice( 0, colon ).trim(),
+ type: trimmed.slice( colon + 1 ).trim()
+ } );
+
+ }
+ return fields.length ? layoutUniformStruct( fields ) : null;
+
+}
+
+/**
+ * TSL `uniformArray` only binds `array`. Flatten `array` to
+ * `array` and add an unpack helper for indexed struct access.
+ */
+function flattenLightDataUniform( wgsl ) {
+
+ const layout = parseLightDataStruct( wgsl );
+ if ( ! layout ) return { wgsl, layout: null };
+
+ const maxMatch = wgsl.match( /array/ );
+ let maxCount = 1;
+ if ( maxMatch ) {
+
+ const sym = maxMatch[ 1 ];
+ const constM = wgsl.match( new RegExp( `const\\s+${ escapeRegExp( sym ) }:\\s*i32\\s*=\\s*(\\d+)` ) );
+ maxCount = constM ? parseInt( constM[ 1 ], 10 ) : 1;
+
+ }
+ const flatCount = maxCount * layout.vec4Stride;
+ const stride = layout.vec4Stride;
+
+ const unpackLines = [ 'var out: LightData;' ];
+ for ( const p of layout.placements ) {
+
+ const vec4Index = Math.floor( p.offset / 16 );
+ const comp = [ 'x', 'y', 'z', 'w' ][ ( p.offset % 16 ) / 4 ];
+ if ( p.type === 'vec3f' || p.type === 'vec3' ) {
+
+ unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].xyz;` );
+
+ } else if ( p.type === 'i32' ) {
+
+ unpackLines.push( `out.${ p.name } = bitcast(data[base + ${ vec4Index }u].${ comp });` );
+
+ } else if ( p.type === 'f32' ) {
+
+ unpackLines.push( `out.${ p.name } = data[base + ${ vec4Index }u].${ comp };` );
+
+ }
+
+ }
+ const helper = [
+ `fn mx_lightData_at(data: array, index: i32) -> LightData {`,
+ `\tlet base: u32 = u32(index) * ${ stride }u;`,
+ ...unpackLines.map( ( l ) => `\t${ l }` ),
+ '\treturn out;',
+ '}'
+ ].join( '\n' );
+
+ let out = wgsl.replace( /struct\s+LightData\s*\{/, `${ helper }\n\nstruct LightData {` );
+ out = out.replace( /array]+>/g, `array` );
+ out = out.replace( /u_lightData\[([^\]]+)\]/g, 'mx_lightData_at(u_lightData, $1)' );
+ return { wgsl: out, layout };
+
+}
+
+function vec3ToArray( v ) {
+
+ if ( Array.isArray( v ) ) return v;
+ if ( v && v.x != null ) return [ v.x, v.y, v.z ];
+ return [ 0, 0, 0 ];
+
+}
+
+// Reused buffer for packing i32 fields into vec4f slots (WGSL unpack uses bitcast).
+const _intBitsView = new DataView( new ArrayBuffer( 4 ) );
+
+/** Store an i32 in a float32 uniform slot preserving bit pattern for WGSL bitcast. */
+function floatFromIntBits( i32 ) {
+
+ _intBitsView.setInt32( 0, i32 | 0, true );
+ return _intBitsView.getFloat32( 0, true );
+
+}
+
+/**
+ * Pack light slots into a flat vec4 array matching WGSL uniform struct layout.
+ * Inactive slots are zeroed so the WGSL light loop can skip them via u_numActiveLightSources.
+ */
+export function packLightDataToVec4Array( lightData, maxCount, layout ) {
+
+ const vec4Count = maxCount * layout.vec4Stride;
+ const floats = new Array( vec4Count * 4 ).fill( 0 );
+ const floatStride = layout.vec4Stride * 4;
+ for ( let i = 0; i < maxCount; i ++ ) {
+
+ const l = lightData && lightData[ i ];
+ const base = i * floatStride;
+ if ( ! l ) continue;
+ for ( const p of layout.placements ) {
+
+ const floatIndex = base + p.offset / 4;
+ if ( p.type === 'i32' || p.type === 'u32' ) {
+
+ let intVal = 0;
+ if ( p.name === 'light_type' || p.name === 'type' ) {
+
+ intVal = l.light_type != null ? l.light_type : ( l.type | 0 );
+
+ } else if ( l[ p.name ] != null ) {
+
+ intVal = l[ p.name ] | 0;
+
+ }
+ floats[ floatIndex ] = floatFromIntBits( intVal );
+
+ } else if ( p.name === 'direction' ) {
+
+ const d = vec3ToArray( l.direction );
+ floats[ floatIndex ] = d[ 0 ];
+ floats[ floatIndex + 1 ] = d[ 1 ];
+ floats[ floatIndex + 2 ] = d[ 2 ];
+
+ } else if ( p.name === 'color' ) {
+
+ const c = vec3ToArray( l.color );
+ floats[ floatIndex ] = c[ 0 ];
+ floats[ floatIndex + 1 ] = c[ 1 ];
+ floats[ floatIndex + 2 ] = c[ 2 ];
+
+ } else if ( p.name === 'intensity' ) {
+
+ floats[ floatIndex ] = l.intensity != null ? l.intensity : 0;
+
+ }
+
+ }
+
+ }
+ const out = [];
+ for ( let i = 0; i < vec4Count; i ++ ) {
+
+ const b = i * 4;
+ out.push( { x: floats[ b ], y: floats[ b + 1 ], z: floats[ b + 2 ], w: floats[ b + 3 ] } );
+
+ }
+ return out;
+
+}
+
+/**
+ * Extract a top-level `fn ( ) -> { }` by name.
+ * Returns { start, end, params, ret, body } where [start,end) spans the whole
+ * definition in `src`. Throws if not found.
+ */
+function extractFunction( src, name ) {
+
+ const re = new RegExp( `\\bfn\\s+${ escapeRegExp( name ) }\\s*\\(`, 'g' );
+ const m = re.exec( src );
+ if ( m === null ) throw new Error( `mxtsladapter: entry function '${ name }' not found` );
+
+ const start = m.index;
+ let i = m.index + m[ 0 ].length - 1; // at '('
+
+ // Match the parameter-list parentheses.
+ let depth = 0;
+ const paramStart = i + 1;
+ let paramEnd = - 1;
+ for ( ; i < src.length; i ++ ) {
+
+ if ( src[ i ] === '(' ) depth ++;
+ else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) { paramEnd = i; break; } }
+
+ }
+
+ if ( paramEnd < 0 ) throw new Error( `mxtsladapter: unterminated parameter list for '${ name }'` );
+
+ const params = src.slice( paramStart, paramEnd );
+
+ // Optional `-> type` then the body braces.
+ let j = skipWs( src, paramEnd + 1 );
+ let ret = '';
+ if ( src[ j ] === '-' && src[ j + 1 ] === '>' ) {
+
+ j = skipWs( src, j + 2 );
+ const retStart = j;
+ while ( j < src.length && src[ j ] !== '{' ) j ++;
+ ret = src.slice( retStart, j ).trim();
+
+ }
+
+ if ( src[ j ] !== '{' ) throw new Error( `mxtsladapter: missing body for '${ name }'` );
+
+ const bodyStart = j;
+ depth = 0;
+ let bodyEnd = - 1;
+ for ( ; j < src.length; j ++ ) {
+
+ if ( src[ j ] === '{' ) depth ++;
+ else if ( src[ j ] === '}' ) { depth --; if ( depth === 0 ) { bodyEnd = j + 1; break; } }
+
+ }
+
+ if ( bodyEnd < 0 ) throw new Error( `mxtsladapter: unterminated body for '${ name }'` );
+
+ return {
+ start,
+ end: bodyEnd,
+ params: params.trim(),
+ ret,
+ body: src.slice( bodyStart + 1, bodyEnd - 1 ) // inside the outer braces
+ };
+
+}
+
+/** Remove every line that declares a `@group(...) @binding(...)` resource. */
+function removeBindingDecls( src ) {
+
+ return src
+ .split( '\n' )
+ .filter( ( line ) => ! /^\s*@group\s*\(/.test( line ) )
+ .join( '\n' );
+
+}
+
+/** Index of the `)` matching the `(` at `openIdx` in `src`. */
+function matchParen( src, openIdx ) {
+
+ let depth = 0;
+ for ( let i = openIdx; i < src.length; i ++ ) {
+
+ if ( src[ i ] === '(' ) depth ++;
+ else if ( src[ i ] === ')' ) { depth --; if ( depth === 0 ) return i; }
+
+ }
+ return - 1;
+
+}
+
+/**
+ * Thread private/global HW resources through the call chain.
+ *
+ * The MaterialX library functions (IBL, transmission, lighting) reference the HW private
+ * uniforms + env maps as *module-scope globals* (`u_envMatrix`, `u_viewPosition`,
+ * `u_lightData`, the env textures, …). Three.js `wgslFn` forbids module-scope resources in
+ * `includes`, so any function that (transitively) touches one of them must receive it as a
+ * parameter, and every call site must forward it.
+ *
+ * Only these private/global resources are threaded. Public material inputs (semantic
+ * `uniform`) and material image textures (semantic `texture`/`sampler`) must NOT be threaded:
+ * they are part of the MaterialX node interface and are already passed explicitly down the
+ * node-graph call chain as named parameters (e.g. `NG_..._surfaceshader_100(base, base_color, …)`).
+ * Threading them would re-append them to functions that already declare them as parameters or
+ * use them as local variable names, producing WGSL "redeclaration of '…'" errors.
+ *
+ * The entry function is left unmodified — `convertToTslPortable` adds the resources to it as
+ * parameters from the manifest, and call sites in the entry body forward them.
+ *
+ * @param {string} wgsl - The full generated WGSL module.
+ * @param {Object} manifest - Generator manifest (uniforms + textures after normalization).
+ * @return {string} The rewritten module (unchanged if there are no private resources).
+ */
+export function threadEnvResources( wgsl, manifest ) {
+
+ // The generator references geometry varyings through the MaterialX vertex-data instance
+ // `vd` (e.g. `vd.normalWorld`). The entry exposes the `VertexData` members as flat
+ // parameters (and the assembler binds them to TSL builtins), so flatten the struct access
+ // to bare member names module-wide. The varyings are then threaded like any other global
+ // below, so library helpers (e.g. the surface-shader node) that read them get them as
+ // parameters forwarded from the entry. `\bvd\.` only matches the vertex-data instance.
+ wgsl = wgsl.replace( /\bvd\./g, '' );
+
+ // A resource is "private/global" (referenced as a module-scope global inside library
+ // helpers, hence threaded) when it is an env map or a non-public uniform. Public material
+ // inputs (`uniform`) and material image textures (`texture`/`sampler`) flow through the
+ // node-graph parameter chain and are excluded.
+ const isPrivateTexture = ( t ) => ( t.semantic || '' ).startsWith( 'env:' );
+ const isPrivateUniform = ( u ) => ( u.semantic || 'uniform' ) !== 'uniform';
+
+ // Ordered resource list (signature decls + call args), data-driven from the manifest.
+ const resourceParts = [];
+
+ // Vertex-data varyings (now bare member names after the `vd.` flatten above). These are
+ // genuine module inputs that helpers reference globally, so they must be threaded too.
+ for ( const p of manifest.entryParams || [] ) {
+
+ if ( ! ( p.semantic || '' ).startsWith( 'varying:' ) ) continue;
+ resourceParts.push( { decl: `${ p.name }: ${ p.type }`, arg: p.name } );
+
+ }
+ for ( const t of manifest.textures || [] ) {
+
+ if ( ! isPrivateTexture( t ) ) continue;
+ resourceParts.push( { decl: `${ t.texture }: ${ t.wgslType }`, arg: t.texture } );
+ resourceParts.push( { decl: `${ t.sampler }: sampler`, arg: t.sampler } );
+
+ }
+ for ( const u of manifest.uniforms || [] ) {
+
+ if ( ! isPrivateUniform( u ) ) continue;
+ resourceParts.push( { decl: `${ u.name }: ${ u.type }`, arg: u.name } );
+
+ }
+ if ( resourceParts.length === 0 ) return wgsl;
+
+ const resourceNames = resourceParts.map( ( p ) => p.arg );
+ const paramsStr = resourceParts.map( ( p ) => p.decl ).join( ', ' );
+ const argsStr = resourceParts.map( ( p ) => p.arg ).join( ', ' );
+
+ // Enumerate all top-level `fn name( params ) ... { body }` definitions.
+ const fnRe = /\bfn\s+([A-Za-z_]\w*)\s*\(/g;
+ const fns = [];
+ for ( let m = fnRe.exec( wgsl ); m !== null; m = fnRe.exec( wgsl ) ) {
+
+ const openParen = m.index + m[ 0 ].length - 1;
+ const closeParen = matchParen( wgsl, openParen );
+ const braceOpen = wgsl.indexOf( '{', closeParen );
+ // Match the body braces to know where this function ends.
+ let depth = 0, braceEnd = - 1;
+ for ( let i = braceOpen; i < wgsl.length; i ++ ) {
+
+ if ( wgsl[ i ] === '{' ) depth ++;
+ else if ( wgsl[ i ] === '}' ) { depth --; if ( depth === 0 ) { braceEnd = i; break; } }
+
+ }
+ fns.push( { name: m[ 1 ], openParen, closeParen, bodyStart: braceOpen, bodyEnd: braceEnd } );
+
+ }
+
+ const byName = Object.fromEntries( fns.map( ( f ) => [ f.name, f ] ) );
+ const bodyOf = ( f ) => wgsl.slice( f.bodyStart, f.bodyEnd + 1 );
+
+ // Fixpoint: a function "needs" resources if its body references one directly, or
+ // calls another function that needs them.
+ const needs = new Set();
+ const refsResource = ( body ) => resourceNames.some( ( n ) => new RegExp( `\\b${ escapeRegExp( n ) }\\b` ).test( body ) );
+ for ( const f of fns ) if ( refsResource( bodyOf( f ) ) ) needs.add( f.name );
+
+ for ( let changed = true; changed; ) {
+
+ changed = false;
+ for ( const f of fns ) {
+
+ if ( needs.has( f.name ) ) continue;
+ const body = bodyOf( f );
+ for ( const callee of needs ) {
+
+ if ( new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(` ).test( body ) ) { needs.add( f.name ); changed = true; break; }
+
+ }
+
+ }
+
+ }
+
+ if ( needs.size === 0 ) return wgsl;
+
+ const entryName = manifest.entry;
+
+ // Collect insertions (index -> text). Apply right-to-left so indices stay valid.
+ const inserts = [];
+
+ // 1. Append params to the signature of each needing function (except the entry).
+ for ( const f of fns ) {
+
+ if ( ! needs.has( f.name ) || f.name === entryName ) continue;
+ const hasParams = wgsl.slice( f.openParen + 1, f.closeParen ).trim().length > 0;
+ inserts.push( { at: f.closeParen, text: ( hasParams ? ', ' : '' ) + paramsStr } );
+
+ }
+
+ // 2. Append args at every call site of a needing function (anywhere in the module).
+ for ( const callee of needs ) {
+
+ const callRe = new RegExp( `\\b${ escapeRegExp( callee ) }\\s*\\(`, 'g' );
+ for ( let m = callRe.exec( wgsl ); m !== null; m = callRe.exec( wgsl ) ) {
+
+ // Skip the definition itself (`fn callee(`).
+ const before = wgsl.slice( Math.max( 0, m.index - 4 ), m.index );
+ if ( /\bfn\s$/.test( before ) ) continue;
+ const open = m.index + m[ 0 ].length - 1;
+ const close = matchParen( wgsl, open );
+ if ( close < 0 ) continue;
+ const hasArgs = wgsl.slice( open + 1, close ).trim().length > 0;
+ inserts.push( { at: close, text: ( hasArgs ? ', ' : '' ) + argsStr } );
+
+ }
+
+ }
+
+ inserts.sort( ( a, b ) => b.at - a.at );
+ let out = wgsl;
+ for ( const ins of inserts ) out = out.slice( 0, ins.at ) + ins.text + out.slice( ins.at );
+ return out;
+
+}
+
+// ── Reflection normalization (neutral C++ → TSL semantics) ─────────────────
+
+function varyingSemantic( name ) {
+
+ if ( name.includes( 'normal' ) ) return 'varying:normalWorld';
+ if ( name.includes( 'tangent' ) ) return 'varying:tangentWorld';
+ if ( name.includes( 'position' ) ) return 'varying:positionWorld';
+ return 'varying:uv';
+
+}
+
+function buildTslSemantics( binding ) {
+
+ const { name, role } = binding;
+ const HOST = {
+ u_viewPosition: 'camera:viewPosition',
+ u_numActiveLightSources: 'light:count',
+ u_lightData: 'light:data',
+ u_envMatrix: 'env:matrix',
+ u_envRadianceMips: 'env:radianceMips',
+ u_envRadianceSamples: 'env:radianceSamples',
+ u_envLightIntensity: 'env:lightIntensity'
+ };
+ if ( HOST[ name ] ) return HOST[ name ];
+ if ( name === 'u_lightDirection' ) return 'light:direction';
+ if ( name === 'u_lightColor' ) return 'light:color';
+ if ( role === 'lightData' ) return 'light:data';
+ if ( role === 'uniform' ) return 'uniform';
+ if ( role === 'texture' ) {
+
+ if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance';
+ if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance';
+ return 'texture';
+
+ }
+ if ( role === 'sampler' ) {
+
+ if ( name.startsWith( 'u_envRadiance' ) ) return 'env:radiance:sampler';
+ if ( name.startsWith( 'u_envIrradiance' ) ) return 'env:irradiance:sampler';
+ return 'sampler';
+
+ }
+ if ( role === 'host' ) return HOST[ name ] || 'host';
+ return 'uniform';
+
+}
+
+/**
+ * Normalize generator reflection into the legacy manifest shape expected by the
+ * TSL bridge. Accepts both the old manifest (entry string + uniforms/textures)
+ * and the new neutral format (entry object + bindings + struct entryParams).
+ *
+ * @param {Object} reflection - Generator reflection or legacy manifest.
+ * @return {Object} Manifest with TSL semantics on uniforms, textures, entryParams.
+ */
+export function normalizeReflection( reflection ) {
+
+ if ( ! reflection ) return reflection;
+
+ // Legacy manifests already carry TSL semantics — return unchanged.
+ if ( reflection.uniforms?.some( ( u ) => u.semantic ) || reflection.textures?.some( ( t ) => t.semantic ) )
+ return reflection;
+
+ if ( ! reflection.bindings ) return reflection;
+
+ const entry = typeof reflection.entry === 'string'
+ ? reflection.entry
+ : reflection.entry?.pixel;
+
+ const uniforms = [];
+ const textures = [];
+ const textureKeys = new Set();
+
+ for ( const b of reflection.bindings ) {
+
+ const semantic = buildTslSemantics( b );
+ if ( b.role === 'texture' ) {
+
+ const key = b.key || b.name.replace( /_texture$/, '' );
+ textures.push( {
+ key,
+ texture: b.name,
+ sampler: null,
+ wgslType: b.type,
+ semantic,
+ file: b.file
+ } );
+ textureKeys.add( key );
+
+ } else if ( b.role === 'sampler' ) {
+
+ const key = b.key || b.name.replace( /_sampler$/, '' );
+ const tex = textures.find( ( t ) => t.key === key );
+ if ( tex ) tex.sampler = b.name;
+
+ } else {
+
+ uniforms.push( {
+ name: b.name,
+ type: b.type,
+ semantic,
+ value: b.value
+ } );
+
+ }
+
+ }
+
+ for ( const t of textures ) {
+
+ if ( ! t.sampler ) t.sampler = `${ t.key }_sampler`;
+
+ }
+
+ let entryParams = reflection.entryParams || [];
+ if ( entryParams.length === 1 && entryParams[ 0 ].members ) {
+
+ entryParams = entryParams[ 0 ].members.map( ( m ) => ( {
+ name: m.name,
+ type: m.type,
+ semantic: varyingSemantic( m.name )
+ } ) );
+
+ } else {
+
+ entryParams = entryParams.map( ( p ) => ( {
+ ...p,
+ semantic: p.semantic || varyingSemantic( p.name )
+ } ) );
+
+ }
+
+ return {
+ entry,
+ output: reflection.output,
+ entryParams,
+ uniforms,
+ textures
+ };
+
+}
+
+// ── Conversion ──────────────────────────────────────────────────────────────
+
+/**
+ * Convert a complete MaterialX WGSL module into a TSL-portable form.
+ *
+ * @param {string} wgsl - The generated WGSL module.
+ * @param {Object} manifest - Resource description emitted by the generator.
+ * @return {{ name:string, entry:string, includes:string, params:Array }}
+ */
+export function convertToTslPortable( wgsl, manifest ) {
+
+ manifest = normalizeReflection( manifest );
+
+ // Thread module-scope resources through the call chain so `includes` receive them
+ // as parameters rather than globals (required by Three.js `wgslFn`).
+ wgsl = threadEnvResources( wgsl, manifest );
+
+ // Three.js uniformArray only supports primitive array types (vec4/mat4), not struct arrays.
+ const lightFlatten = flattenLightDataUniform( wgsl );
+ wgsl = lightFlatten.wgsl;
+ const lightLayout = lightFlatten.layout;
+
+ const entryName = manifest.entry;
+ const fn = extractFunction( wgsl, entryName );
+
+ const uniforms = manifest.uniforms || [];
+ const textures = manifest.textures || [];
+ const entryParams = manifest.entryParams || [];
+
+ // The generated entry already takes the geometry varyings as parameters and
+ // references uniforms/textures by bare name as module-scope `@group/@binding`
+ // resources. Hoist those resources into the parameter list (bodies use bare
+ // names, so no in-body rewriting is needed) and strip the binding declarations.
+ const newParams = [];
+ const params = [];
+
+ // 1. Existing entry parameters (varyings). Neutral reflection may pass vertex
+ // data as a single struct parameter — flatten it for wgslFn and rewrite the body.
+ const semanticByName = Object.fromEntries( entryParams.map( ( p ) => [ p.name, p.semantic ] ) );
+ const flatParams = splitParams( fn.params );
+ const isStructEntry = flatParams.length === 1 && entryParams.length > 0 &&
+ entryParams[ 0 ].name !== flatParams[ 0 ].name;
+ let body = fn.body;
+ if ( isStructEntry ) {
+
+ const structName = flatParams[ 0 ].name;
+ for ( const m of entryParams ) {
+
+ newParams.push( `${ m.name }: ${ m.type }` );
+ params.push( { name: m.name, type: m.type, semantic: m.semantic || varyingSemantic( m.name ) } );
+
+ }
+ body = body.replace( new RegExp( `\\b${ escapeRegExp( structName ) }\\.`, 'g' ), '' );
+
+ } else {
+
+ for ( const p of flatParams ) {
+
+ newParams.push( `${ p.name }: ${ p.type }` );
+ params.push( { name: p.name, type: p.type, semantic: semanticByName[ p.name ] || 'varying:uv' } );
+
+ }
+
+ }
+
+ // 2. Uniforms become individual parameters (bare names already used in body).
+ // Most carry semantic 'uniform'; surface lighting inputs carry camera:/light:.
+ for ( const u of uniforms ) {
+
+ let paramType = u.type;
+ if ( ( u.semantic || '' ) === 'light:data' && lightLayout ) {
+
+ const { flatCount } = parseStructArrayType( u.type, wgsl, null, null, lightLayout );
+ paramType = `array`;
+
+ }
+ newParams.push( `${ u.name }: ${ paramType }` );
+ params.push( { name: u.name, type: paramType, semantic: u.semantic || 'uniform', value: u.value } );
+
+ }
+
+ // 3. Each texture contributes a texture + sampler parameter pair. Material image
+ // textures carry semantic 'texture'; env (IBL) maps carry 'env:radiance' /
+ // 'env:irradiance' so the assembler can bind them from the environment instead.
+ for ( const tex of textures ) {
+
+ const texSemantic = tex.semantic && tex.semantic.startsWith( 'env:' ) ? tex.semantic : 'texture';
+ const sampSemantic = texSemantic === 'texture' ? 'sampler' : texSemantic + ':sampler';
+ newParams.push( `${ tex.texture }: ${ tex.wgslType }` );
+ newParams.push( `${ tex.sampler }: sampler` );
+ params.push( { name: tex.texture, type: tex.wgslType, semantic: texSemantic, key: tex.key } );
+ params.push( { name: tex.sampler, type: 'sampler', semantic: sampSemantic, key: tex.key } );
+
+ }
+
+ const retArrow = fn.ret ? ` -> ${ fn.ret }` : '';
+ const entry = `fn ${ entryName }( ${ newParams.join( ', ' ) } )${ retArrow } {${ body }}`;
+
+ // Includes: the original module minus the entry and the binding declarations.
+ let includes = wgsl.slice( 0, fn.start ) + wgsl.slice( fn.end );
+ includes = removeBindingDecls( includes ).trim();
+
+ return { name: entryName, entry, includes, params };
+
+}
+
+/** Split a WGSL parameter list ("a: T1, b: T2") into [{name,type}]. */
+function splitParams( params ) {
+
+ const out = [];
+ let depth = 0, start = 0;
+ const pieces = [];
+ for ( let i = 0; i < params.length; i ++ ) {
+
+ const c = params[ i ];
+ if ( c === '<' || c === '(' ) depth ++;
+ else if ( c === '>' || c === ')' ) depth --;
+ else if ( c === ',' && depth === 0 ) { pieces.push( params.slice( start, i ) ); start = i + 1; }
+
+ }
+ const tail = params.slice( start ).trim();
+ if ( tail ) pieces.push( tail );
+
+ for ( const piece of pieces ) {
+
+ const colon = piece.indexOf( ':' );
+ if ( colon < 0 ) continue;
+ out.push( { name: piece.slice( 0, colon ).trim(), type: piece.slice( colon + 1 ).trim() } );
+
+ }
+ return out;
+
+}
+
+// ── Material assembly ────────────────────────────────────────────────────────
+
+/**
+ * Build a Three.js NodeMaterial from MaterialX WGSL + manifest.
+ *
+ * @param {Object} options
+ * @param {Object} options.THREE - The `three/webgpu` namespace.
+ * @param {Object} options.TSL - The `three/tsl` namespace.
+ * @param {string} options.wgsl - Generated WGSL module.
+ * @param {Object} options.manifest - Generator manifest.
+ * @param {Object} [options.textures] - Map of texture key -> Texture.
+ * @param {Node} [options.uvNode] - Optional uv node (defaults to TSL `uv()`).
+ * @param {boolean} [options.useGeometryTangent] - Use TSL `tangentWorld` when geometry tangents exist.
+ * @return {NodeMaterial}
+ */
+export function createMxWgslMaterial( { THREE, TSL, wgsl, manifest, textures = {}, uvNode = null, light = null, lightData = null, numLights = null, environment = null, useGeometryTangent = false } ) {
+
+ manifest = normalizeReflection( manifest );
+
+ const { wgslFn, wgsl: wgslCode, uniform, uniformArray, texture, sampler, uv, normalWorld, positionWorld, cameraPosition, tangentWorld: tangentWorldBuiltin, vec3, float, select } = TSL;
+
+ // IBL environment: equirect radiance (mipped) + irradiance maps and FIS parameters.
+ // When the shader was generated with FIS but no environment is supplied, bind a 1×1
+ // black fallback so the module still compiles (IBL simply contributes nothing).
+ const env = environment || {};
+ const fallbackEnvTex = () => {
+
+ const t = new THREE.DataTexture( new Uint8Array( [ 0, 0, 0, 255 ] ), 1, 1 );
+ t.needsUpdate = true;
+ return t;
+
+ };
+ const envRadianceTex = env.radiance || fallbackEnvTex();
+ const envIrradianceTex = env.irradiance || envRadianceTex;
+ // HDR env maps must stay linear (see viewer buildEnvironment).
+ envRadianceTex.colorSpace = THREE.NoColorSpace;
+ envIrradianceTex.colorSpace = THREE.NoColorSpace;
+ const envTexFor = ( semantic ) => semantic.startsWith( 'env:irradiance' ) ? envIrradianceTex : envRadianceTex;
+ const fallbackMatTex = () => {
+
+ const t = new THREE.DataTexture( new Uint8Array( [ 128, 128, 128, 255 ] ), 1, 1 );
+ // DataTexture defaults to Nearest+Nearest; Three.js treats that as "unfilterable" and
+ // omits nodeUniformN_sampler bindings while MaterialX WGSL still passes samplers.
+ t.magFilter = THREE.LinearFilter;
+ t.minFilter = THREE.LinearFilter;
+ t.needsUpdate = true;
+ return t;
+
+ };
+
+ // Three.js only emits sampler bindings for filterable textures (see WebGPUNodeBuilder
+ // needsSampler). MaterialX WGSL always declares matching sampler parameters.
+ const ensureFilterableTexture = ( tex ) => {
+
+ if ( ! tex ) return tex;
+ if ( tex.magFilter === THREE.NearestFilter ) tex.magFilter = THREE.LinearFilter;
+ if ( tex.minFilter === THREE.NearestFilter || tex.minFilter === THREE.NearestMipmapNearestFilter ) {
+
+ tex.minFilter = tex.generateMipmaps !== false ? THREE.LinearMipmapLinearFilter : THREE.LinearFilter;
+
+ }
+ return tex;
+
+ };
+
+ // A robust world-space tangent. MaterialX BSDFs orthogonalize the tangent against
+ // the normal (`normalize(T - dot(T,N)*N)`), which yields NaN if T is zero or parallel
+ // to N — and most geometries (e.g. TorusKnotGeometry) carry no tangent attribute, so
+ // a raw `tangentWorld` accessor would be degenerate. Derive an arbitrary orthonormal
+ // tangent from the normal instead; for isotropic GGX the exact direction is irrelevant.
+ function derivedTangentWorld() {
+
+ const n = normalWorld.normalize();
+ // Reference axis least aligned with the normal, to avoid a parallel cross product.
+ const ref = select( n.y.abs().lessThan( float( 0.99 ) ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) );
+ return ref.cross( n ).normalize();
+
+ }
+ // MaterialXView computes tangents for indexed geometry; use them when available for
+ // anisotropic parity with the WebGL path. Fall back to a derived tangent otherwise.
+ const tangentWorld = useGeometryTangent ? tangentWorldBuiltin : derivedTangentWorld();
+
+ const converted = convertToTslPortable( wgsl, manifest );
+ const wgslForConsts = converted.includes + '\n' + converted.entry;
+
+ const includesNode = converted.includes.length ? [ wgslCode( converted.includes ) ] : [];
+ const entryFn = wgslFn( converted.entry, includesNode );
+
+ const builtin = {
+ 'varying:uv': () => ( uvNode || uv() ),
+ 'varying:normalWorld': () => normalWorld,
+ 'varying:positionWorld': () => positionWorld,
+ 'varying:tangentWorld': () => tangentWorld,
+ 'camera:viewPosition': () => cameraPosition
+ };
+
+ const args = {};
+ // One TSL texture() node per manifest texture key. wgslFn passes texture and sampler as
+ // separate WGSL parameters; calling texture() twice (even with the same THREE.Texture)
+ // creates two binding slots (nodeUniformN + nodeUniformN+1_sampler) and breaks pairing.
+ const textureNodes = {};
+ const textureNodeFor = ( key, threeTex ) => {
+
+ if ( ! textureNodes[ key ] ) textureNodes[ key ] = texture( ensureFilterableTexture( threeTex ) );
+ return textureNodes[ key ];
+
+ };
+
+ for ( const p of converted.params ) {
+
+ if ( p.semantic === 'uniform' ) {
+
+ args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type );
+
+ } else if ( p.semantic === 'light:direction' ) {
+
+ args[ p.name ] = uniform( ( light && light.direction ) || toUniformValue( THREE, p ) );
+
+ } else if ( p.semantic === 'light:color' ) {
+
+ args[ p.name ] = uniform( ( light && light.color ) || toUniformValue( THREE, p ) );
+
+ } else if ( p.semantic === 'light:count' ) {
+
+ args[ p.name ] = uniform( ( numLights != null ? numLights : p.value ) | 0, 'int' );
+
+ } else if ( p.semantic === 'light:data' ) {
+
+ const layout = parseLightDataStruct( wgslForConsts );
+ const { count, flatCount } = parseStructArrayType( p.type, wgslForConsts, null, lightData, layout );
+ const packed = layout
+ ? packLightDataToVec4Array( lightData || p.value, count, layout )
+ : new Array( flatCount ).fill( { x: 0, y: 0, z: 0, w: 0 } );
+ args[ p.name ] = uniformArray( packed, 'vec4' );
+
+ } else if ( p.semantic === 'host' ) {
+
+ args[ p.name ] = makeUniformNode( uniform, toUniformValue( THREE, p ), p.type );
+
+ } else if ( p.semantic === 'texture' ) {
+
+ args[ p.name ] = textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() );
+
+ } else if ( p.semantic === 'sampler' ) {
+
+ args[ p.name ] = sampler( textureNodeFor( p.key, textures[ p.key ] || fallbackMatTex() ) );
+
+ } else if ( p.semantic === 'env:radiance' || p.semantic === 'env:irradiance' ) {
+
+ args[ p.name ] = textureNodeFor( p.key, envTexFor( p.semantic ) );
+
+ } else if ( p.semantic === 'env:radiance:sampler' || p.semantic === 'env:irradiance:sampler' ) {
+
+ const envSemantic = p.semantic.replace( ':sampler', '' );
+ args[ p.name ] = sampler( textureNodeFor( p.key, envTexFor( envSemantic ) ) );
+
+ } else if ( p.semantic === 'env:matrix' ) {
+
+ args[ p.name ] = uniform( env.matrix || matrixFromArray( THREE, p.value ) );
+
+ } else if ( p.semantic === 'env:radianceSamples' ) {
+
+ args[ p.name ] = uniform( ( env.samples != null ? env.samples : p.value ) | 0, 'int' );
+
+ } else if ( p.semantic === 'env:radianceMips' ) {
+
+ args[ p.name ] = uniform( ( env.mips != null ? env.mips : p.value ) | 0, 'int' );
+
+ } else if ( p.semantic === 'env:lightIntensity' ) {
+
+ args[ p.name ] = uniform( env.intensity != null ? env.intensity : p.value );
+
+ } else if ( p.semantic && ( p.semantic.startsWith( 'varying:' ) || p.semantic.startsWith( 'camera:' ) ) ) {
+
+ const make = builtin[ p.semantic ];
+ if ( ! make ) throw new Error( `mxtsladapter: unknown builtin semantic '${ p.semantic }'` );
+ args[ p.name ] = make();
+
+ } else {
+
+ throw new Error( `mxtsladapter: unhandled parameter semantic '${ p.semantic }' for '${ p.name }'` );
+
+ }
+
+ }
+
+ const material = new THREE.MeshBasicNodeMaterial();
+ // MaterialX WGSL already includes direct lights + IBL; do not run Three.js scene lighting.
+ material.lights = false;
+ material.fog = false;
+ material.colorNode = entryFn( args );
+ // Bound argument nodes, keyed by parameter name. `createMxWgslGUI` reads these to
+ // drive live uniform edits, so this is a functional binding handle, not debug state.
+ material.userData.mxArgs = args;
+ return material;
+
+}
+
+function toUniformValue( THREE, p ) {
+
+ const v = p.value;
+ if ( v === undefined || v === null ) {
+
+ // Sensible zero/identity defaults by type.
+ if ( p.type === 'f32' || p.type === 'i32' || p.type === 'u32' ) return 0;
+ if ( p.type === 'vec2f' ) return new THREE.Vector2();
+ if ( p.type === 'vec4f' ) return new THREE.Vector4();
+ return new THREE.Vector3();
+
+ }
+
+ if ( typeof v === 'boolean' && ( p.type === 'i32' || p.type === 'u32' ) ) return v ? 1 : 0;
+
+ if ( Array.isArray( v ) ) {
+
+ if ( v.length === 2 ) return new THREE.Vector2( v[ 0 ], v[ 1 ] );
+ if ( v.length === 3 ) return new THREE.Vector3( v[ 0 ], v[ 1 ], v[ 2 ] );
+ if ( v.length === 4 ) return new THREE.Vector4( v[ 0 ], v[ 1 ], v[ 2 ], v[ 3 ] );
+ if ( v.length === 16 ) return matrixFromArray( THREE, v );
+
+ }
+
+ return v;
+
+}
+
+/** Bind a TSL uniform node with the correct scalar type for WGSL i32/u32 host inputs. */
+function makeUniformNode( uniformFn, value, wgslType ) {
+
+ if ( wgslType === 'i32' || wgslType === 'u32' ) return uniformFn( value | 0, 'int' );
+ return uniformFn( value );
+
+}
+
+/** Build a THREE.Matrix4 from a 16-element column-major array (WGSL mat4x4f layout). */
+function matrixFromArray( THREE, v ) {
+
+ const m = new THREE.Matrix4();
+ if ( Array.isArray( v ) && v.length === 16 ) {
+
+ // Three stores column-major internally; WGSL mat4x4f is also column-major, so the
+ // array maps directly into Matrix4.elements.
+ m.elements = v.slice();
+
+ }
+ return m;
+
+}
+
+// ── Live parameter editor ─────────────────────────────────────────────────────
+
+// Slider range heuristics for scalar surface inputs, keyed by name substring.
+function scalarRange( name ) {
+
+ if ( /IOR$/.test( name ) ) return [ 1, 3 ];
+ if ( /thickness/.test( name ) ) return [ 0, 2000 ]; // thin-film, nanometres
+ if ( /roughness|metalness|anisotropy|rotation|affect|walled/.test( name ) ) return [ 0, 1 ];
+ if ( /emission|scale|depth|dispersion/.test( name ) ) return [ 0, 5 ];
+ return [ 0, 1 ];
+
+}
+
+const GUI_CATEGORIES = [ 'base', 'diffuse', 'metalness', 'specular', 'transmission',
+ 'subsurface', 'sheen', 'coat', 'thin_film', 'thin_walled', 'emission', 'opacity' ];
+
+function guiCategory( stripped ) {
+
+ for ( const c of GUI_CATEGORIES ) if ( stripped === c || stripped.startsWith( c + '_' ) ) return c;
+ return 'other';
+
+}
+
+/**
+ * Build a live lil-gui editor for a material created by `createMxWgslMaterial`.
+ *
+ * Drives the bound TSL uniform nodes (`material.userData.mxArgs`) directly, so edits take
+ * effect on the next render with no rebuild. Surface inputs are read from the manifest and
+ * grouped into folders by closure (base / specular / coat / …); the directional light and
+ * environment intensity get their own folder.
+ *
+ * @param {Object} options
+ * @param {Function} options.GUI - The lil-gui constructor.
+ * @param {Object} options.THREE - The `three/webgpu` namespace.
+ * @param {NodeMaterial} options.material - Material from `createMxWgslMaterial`.
+ * @param {Object} options.manifest - The generator manifest used to build the material.
+ * @param {GUI} [options.gui] - Existing gui to populate (otherwise a new one is created).
+ * @return {GUI}
+ */
+export function createMxWgslGUI( { GUI, THREE, material, manifest, gui = null } ) {
+
+ manifest = normalizeReflection( manifest );
+ gui = gui || new GUI();
+ const args = material.userData.mxArgs || {};
+
+ const folders = {};
+ const folderFor = ( parent, key, label ) => {
+
+ const id = parent + '/' + key;
+ if ( ! folders[ id ] ) { folders[ id ] = ( parent === '' ? gui : folders[ parent ] ).addFolder( label ); folders[ id ].close(); }
+ return folders[ id ];
+
+ };
+
+ // One control bound to a uniform node's `.value`.
+ const addControl = ( folder, node, type, name, value, label, range ) => {
+
+ if ( type === 'vec3f' && /color|scatter/.test( name ) ) {
+
+ const proxy = { c: Array.isArray( value ) ? value.slice() : [ value.x, value.y, value.z ] };
+ folder.addColor( proxy, 'c' ).name( label ).onChange( ( v ) => node.value.fromArray( v ) );
+
+ } else if ( type === 'vec3f' ) {
+
+ const sub = folder.addFolder( label ); sub.close();
+ const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ] } : { x: value.x, y: value.y, z: value.z };
+ const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2;
+ for ( const k of [ 'x', 'y', 'z' ] )
+ sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z ) );
+
+ } else if ( type === 'bool' ) {
+
+ const proxy = { v: !! value };
+ folder.add( proxy, 'v' ).name( label ).onChange( ( v ) => { node.value = v; } );
+
+ } else if ( type === 'vec2f' ) {
+
+ const sub = folder.addFolder( label ); sub.close();
+ const proxy = Array.isArray( value ) ? { x: value[ 0 ], y: value[ 1 ] } : { x: value.x, y: value.y };
+ const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2;
+ for ( const k of [ 'x', 'y' ] )
+ sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y ) );
+
+ } else if ( type === 'vec4f' ) {
+
+ const sub = folder.addFolder( label ); sub.close();
+ const proxy = Array.isArray( value )
+ ? { x: value[ 0 ], y: value[ 1 ], z: value[ 2 ], w: value[ 3 ] }
+ : { x: value.x, y: value.y, z: value.z, w: value.w };
+ const lo = range ? range[ 0 ] : 0, hi = range ? range[ 1 ] : 2;
+ for ( const k of [ 'x', 'y', 'z', 'w' ] )
+ sub.add( proxy, k, lo, hi ).onChange( () => node.value.set( proxy.x, proxy.y, proxy.z, proxy.w ) );
+
+ } else if ( type === 'f32' || type === 'i32' || type === 'u32' ) {
+
+ const [ lo, hi ] = range || scalarRange( name );
+ const proxy = { v: Number( value ) || 0 };
+ const ctrl = folder.add( proxy, 'v', lo, hi ).name( label ).onChange( ( v ) => { node.value = v; } );
+ if ( type === 'i32' || type === 'u32' ) ctrl.step( 1 );
+
+ }
+
+ };
+
+ // Surface inputs (semantic 'uniform'), grouped by closure category.
+ const surface = folderFor( '', 'surface', 'MaterialX surface' );
+ surface.open();
+ for ( const u of manifest.uniforms || [] ) {
+
+ if ( u.semantic !== 'uniform' ) continue;
+ const node = args[ u.name ];
+ if ( ! node || u.type === 'mat4x4f' ) continue;
+ const stripped = u.name.replace( /^[A-Za-z][A-Za-z0-9]*_/, '' ); // drop node prefix (e.g. 'ss_')
+ const cat = guiCategory( stripped );
+ const folder = cat === 'other' ? surface : folderFor( '/surface', cat, cat.replace( /_/g, ' ' ) );
+ addControl( folder, node, u.type, u.name, u.value, stripped, null );
+
+ }
+
+ // Lighting + environment.
+ const lighting = folderFor( '', 'lighting', 'Lighting & environment' );
+ for ( const u of manifest.uniforms || [] ) {
+
+ const node = args[ u.name ];
+ if ( ! node ) continue;
+ if ( u.semantic === 'light:direction' ) addControl( lighting, node, 'vec3f', 'dir_vec', u.value, 'light direction', [ - 1, 1 ] );
+ else if ( u.semantic === 'light:color' ) addControl( lighting, node, 'vec3f', 'light_rgb', u.value, 'light color', [ 0, 10 ] );
+ else if ( u.semantic === 'env:lightIntensity' ) addControl( lighting, node, 'f32', 'env_intensity', u.value, 'env intensity', [ 0, 3 ] );
+
+ }
+
+ return gui;
+
+}
diff --git a/javascript/MaterialXView/source/viewer.js b/javascript/MaterialXView/source/viewer.js
index 5cfce0bcb3..2590043316 100644
--- a/javascript/MaterialXView/source/viewer.js
+++ b/javascript/MaterialXView/source/viewer.js
@@ -10,6 +10,8 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
import { prepareEnvTexture, getLightRotation, findLights, registerLights, getUniformValues } from './helper.js'
import { Group } from 'three';
import GUI from 'lil-gui';
+import { createMxWgslMaterial, createMxWgslGUI, normalizeReflection } from './mxtsladapter.js';
+import { buildWgslManifest } from './wgslmanifest.js';
const ALL_GEOMETRY_SPECIFIER = "*";
const NO_GEOMETRY_SPECIFIER = "";
@@ -18,6 +20,96 @@ const DAG_PATH_SEPERATOR = "/";
// Logging toggle
var logDetailedTime = false;
+/**
+ * Configure genContext for WebGPU material generation.
+ * @returns {boolean} Whether the surface is transparent.
+ */
+function configureWebGPUGenContext(mx, gen, genContext, elem)
+{
+ // WebGPU: leave surface color linear; WebGPURenderer converts working space → sRGB for the canvas.
+ // WebGL RawShaderMaterial writes shader output directly, so that path keeps hwSrgbEncodeOutput=true.
+ genContext.getOptions().hwSrgbEncodeOutput = false;
+ const isTransparent = mx.isTransparentSurface(elem, gen.getTarget());
+ genContext.getOptions().hwTransparency = isTransparent;
+ genContext.getOptions().shaderInterfaceType = mx.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
+ return isTransparent;
+}
+
+/**
+ * Generate WGSL pixel shader and manifest for a renderable element.
+ */
+function generateWebGPUShader(mx, gen, genContext, elem)
+{
+ const shader = gen.generate(elem.getNamePath(), elem, genContext);
+ const wgsl = shader.getSourceCode('pixel');
+ // The in-repo WgslShaderGenerator does not emit a manifest; reconstruct it in JS from
+ // the generated WGSL text + the Shader's uniform ports (see wgslmanifest.js).
+ const manifest = buildWgslManifest(shader, wgsl);
+ return { shader, wgsl, manifest };
+}
+
+/**
+ * Build the first directional light payload for the TSL bridge.
+ */
+function buildDirectionalLight(lightData)
+{
+ if (!lightData || !lightData.length) return null;
+ const l = lightData[0];
+ return {
+ direction: l.direction.clone(),
+ color: l.color.clone().multiplyScalar(l.intensity)
+ };
+}
+
+/**
+ * Build IBL environment payload for the TSL bridge.
+ */
+function buildEnvironment(radianceTexture, irradianceTexture, matrix, THREE)
+{
+ // HDR env maps are linear radiance data; disable Three.js sRGB decode on the TSL path
+ // (same rationale as buildTextureMap for material textures).
+ radianceTexture.colorSpace = THREE.NoColorSpace;
+ irradianceTexture.colorSpace = THREE.NoColorSpace;
+ const mips = Math.trunc(Math.log2(Math.max(radianceTexture.image.width, radianceTexture.image.height))) + 1;
+ return {
+ radiance: radianceTexture,
+ irradiance: irradianceTexture,
+ samples: 16,
+ mips,
+ intensity: 1.0,
+ matrix
+ };
+}
+
+/**
+ * Map manifest texture keys to loaded THREE.Texture objects.
+ *
+ * MaterialXView registers no color-management system, so the generated shader performs no
+ * sRGB->linear input transform, and the WebGL RawShaderMaterial samples textures raw (the
+ * stored sRGB bytes are used directly). A TSL `texture()` node, by contrast, decodes to
+ * linear working space when `texture.colorSpace` is sRGB. To stay pixel-identical with the
+ * WebGL reference we therefore force `NoColorSpace` so WebGPU samples raw as well. (Proper
+ * color management would mean registering a CMS so BOTH backends convert in-shader; that is
+ * a separate change that would alter the existing WebGL appearance.)
+ */
+function buildTextureMap(manifest, uniforms, THREE)
+{
+ // C++ reflection uses { bindings: [...] }; normalize before reading textures[].
+ manifest = normalizeReflection(manifest);
+ const textures = {};
+ for (const tex of (manifest.textures || []))
+ {
+ if (tex.semantic !== 'texture') continue;
+ const u = uniforms[tex.key];
+ if (u && u.value)
+ {
+ u.value.colorSpace = THREE.NoColorSpace;
+ textures[tex.key] = u.value;
+ }
+ }
+ return textures;
+}
+
/*
Scene management
*/
@@ -243,11 +335,18 @@ export class Scene
updateObjectUniforms(child, material, camera)
{
if (!child || !material || !camera) return;
+
+ // The WebGPU NodeMaterial has no `material.uniforms` block: its per-object world
+ // transforms come from TSL accessors (positionWorld / normalWorld / cameraPosition)
+ // and editor edits drive the TSL uniform nodes directly, so this path is a no-op for
+ // it. The guards below keep the WebGL (RawShaderMaterial) updates working unchanged.
const uniforms = material.uniforms;
if (!uniforms) return;
- uniforms.u_worldMatrix.value = child.matrixWorld;
- uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
+ if (uniforms.u_worldMatrix)
+ uniforms.u_worldMatrix.value = child.matrixWorld;
+ if (uniforms.u_viewProjectionMatrix)
+ uniforms.u_viewProjectionMatrix.value = this.#_viewProjMat.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
if (uniforms.u_viewPosition)
uniforms.u_viewPosition.value = camera.getWorldPosition(this.#_worldViewPos);
@@ -900,6 +999,9 @@ export class Material
//
generateMaterial(matassign, viewer, searchPath, closeUI)
{
+ if (viewer.getBackend() === 'webgpu')
+ return this.generateMaterialWebGPU(matassign, viewer, searchPath, closeUI);
+
var elem = matassign.getMaterial();
var startGenerateMat = performance.now();
@@ -985,6 +1087,68 @@ export class Material
return newMaterial;
}
+ //
+ // WebGPU material generation: emit WGSL with the upstream WgslShaderGenerator, reshape it
+ // into a TSL NodeMaterial via the bridge, and reuse the existing property Editor by
+ // exposing a RawShaderMaterial-style `uniforms` map plus a per-frame sync into the TSL
+ // nodes. Lighting reuses the rig's first directional light + the prepared IBL textures.
+ //
+ generateMaterialWebGPU(matassign, viewer, searchPath, closeUI)
+ {
+ const mx = viewer.getMx();
+ const TSL = viewer.getTSL();
+ const elem = matassign.getMaterial();
+ const gen = viewer.getGenerator();
+ const genContext = viewer.getGenContext();
+ const textureLoader = new THREE.TextureLoader();
+
+ try
+ {
+ const isTransparent = configureWebGPUGenContext(mx, gen, genContext, elem);
+ const { shader, wgsl, manifest } = generateWebGPUShader(mx, gen, genContext, elem);
+
+ const flipV = viewer.getScene().getFlipGeometryV();
+ const uniforms = getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV);
+ const textures = buildTextureMap(manifest, uniforms, THREE);
+
+ const environment = buildEnvironment(
+ viewer.getRadianceTexture(),
+ viewer.getIrradianceTexture(),
+ getLightRotation(),
+ THREE
+ );
+ const light = buildDirectionalLight(viewer.getLightData());
+ const lightData = viewer.getLightData();
+ const numLights = viewer.getLights()?.length ?? 0;
+
+ // Indexed geometry gets computed tangents in updateScene(); use them for anisotropic parity.
+ const material = createMxWgslMaterial({
+ THREE, TSL, wgsl, manifest, light, lightData, numLights, environment, textures,
+ useGeometryTangent: true
+ });
+ material.side = THREE.DoubleSide;
+ material.transparent = isTransparent;
+ material.name = elem.getName();
+
+ const gui = viewer.getEditor().getGUI();
+ this.updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer);
+ return material;
+ }
+ catch (error)
+ {
+ // WGSL generation or TSL assembly failed (e.g. an unsupported node). Surface it and
+ // return a visible magenta fallback so the viewer stays usable instead of crashing.
+ const name = elem ? elem.getName() : '';
+ console.error(`WebGPU material generation failed for '${name}':`,
+ Number.isInteger(error) ? mx.getExceptionMessage(error) : error);
+ const fallback = new THREE.MeshBasicNodeMaterial();
+ fallback.colorNode = TSL.vec3(1.0, 0.0, 1.0);
+ fallback.side = THREE.DoubleSide;
+ fallback.name = name;
+ return fallback;
+ }
+ }
+
clearSoloMaterialUI()
{
for (let i = 0; i < this._materials.length; ++i)
@@ -1043,31 +1207,18 @@ export class Material
// Update property editor for a given MaterialX element, it's shader, and
// Three material
//
- updateEditor(matassign, shader, material, gui, closeUI, viewer)
+ setupMaterialFolder(matassign, elem, gui, closeUI, viewer)
{
- var elem = matassign.getMaterial();
- var materials = this._materials;
-
- const DEFAULT_MIN = 0;
- const DEFAULT_MAX = 100;
-
- var startTime = performance.now();
-
+ const materials = this._materials;
const elemPath = elem.getNamePath();
-
- // Create and cache associated UI
- var matUI = gui.addFolder(elemPath);
+ const matUI = gui.addFolder(elemPath);
matassign.setMaterialUI(matUI);
- let matTitle = matUI.domElement.getElementsByClassName('title')[0];
- // Add a icon to the title to allow for assigning the material to geometry
- // Clicking on the icon will "solo" the material to the geometry.
- // Clicking on the title will open/close the material folder.
+ const matTitle = matUI.domElement.getElementsByClassName('title')[0];
matTitle.innerHTML = "
" + elem.getNamePath();
- let img = matTitle.getElementsByTagName('img')[0];
+ const img = matTitle.getElementsByTagName('img')[0];
if (img)
{
- // Add event listener to icon to call updateSoloMaterial function
img.addEventListener('click', function (event)
{
Material.updateSoloMaterial(viewer, elemPath, materials, event);
@@ -1075,9 +1226,33 @@ export class Material
}
if (closeUI)
- {
matUI.close();
- }
+
+ return matUI;
+ }
+
+ //
+ // WebGPU property editor: manifest-driven TSL uniform nodes via createMxWgslGUI.
+ //
+ updateEditorWebGPU(matassign, material, manifest, gui, closeUI, viewer)
+ {
+ const elem = matassign.getMaterial();
+ const matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer);
+ createMxWgslGUI({ GUI, THREE, material, manifest, gui: matUI });
+ }
+
+ updateEditor(matassign, shader, material, gui, closeUI, viewer)
+ {
+ var elem = matassign.getMaterial();
+
+ const DEFAULT_MIN = 0;
+ const DEFAULT_MAX = 100;
+
+ var startTime = performance.now();
+
+ var matUI = this.setupMaterialFolder(matassign, elem, gui, closeUI, viewer);
+ const elemPath = elem.getNamePath();
+
const uniformBlocks = Object.values(shader.getStage('pixel').getUniformBlocks());
var uniformToUpdate;
const ignoreList = ['u_envRadianceMips', 'u_envRadianceSamples', 'u_alphaThreshold'];
@@ -1539,19 +1714,24 @@ export class Viewer
// Create shader generator, generation context and "base" document which
// contains the standard definition libraries and lighting elements.
//
- async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml)
+ async initialize(mtlxIn, renderer, radianceTexture, irradianceTexture, lightRigXml, opts = {})
{
this.mx = mtlxIn;
-
- // Initialize base document
- this.generator = this.mx.EsslShaderGenerator.create();
+ this.backend = opts.backend || 'webgl';
+ this.tsl = opts.TSL || null;
+
+ // Initialize base document. The WebGPU backend uses the upstream WgslShaderGenerator
+ // (WGSL, consumed by the TSL bridge); WebGL uses the ESSL generator (GLSL).
+ this.generator = (this.backend === 'webgpu')
+ ? this.mx.WgslShaderGenerator.create()
+ : this.mx.EsslShaderGenerator.create();
this.genContext = new this.mx.GenContext(this.generator);
this.document = this.mx.createDocument();
this.stdlib = this.mx.loadStandardLibraries(this.genContext);
this.document.setDataLibrary(this.stdlib);
- this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml);
+ await this.initializeLighting(renderer, radianceTexture, irradianceTexture, lightRigXml);
radianceTexture.mapping = THREE.EquirectangularReflectionMapping;
this.getScene().setBackgroundTexture(radianceTexture);
@@ -1626,6 +1806,16 @@ export class Viewer
return this.mx;
}
+ getBackend()
+ {
+ return this.backend;
+ }
+
+ getTSL()
+ {
+ return this.tsl;
+ }
+
getGenerator()
{
return this.generator;
diff --git a/javascript/MaterialXView/source/wgslmanifest.js b/javascript/MaterialXView/source/wgslmanifest.js
new file mode 100644
index 0000000000..becffe0c71
--- /dev/null
+++ b/javascript/MaterialXView/source/wgslmanifest.js
@@ -0,0 +1,211 @@
+//
+// Copyright Contributors to the MaterialX Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+/**
+ * WGSL reflection manifest builder.
+ *
+ * The in-repo `WgslShaderGenerator` emits WGSL but does not emit the JSON reflection
+ * manifest that the TSL bridge (`mxtsladapter.js`) consumes. Rather than reintroduce
+ * manifest emission into the C++ generator, we reconstruct the manifest here in JS from
+ * two sources that are already available after `generate()`:
+ *
+ * 1. The generated WGSL text — supplies the `@group/@binding` layout, the final WGSL
+ * type spellings, the entry-function names, and the vertex-input struct (varyings).
+ * 2. The `Shader` object — supplies what is NOT present in the WGSL text: each uniform's
+ * default value (`ShaderPort.getValue()`) and the public-vs-host role (the uniform
+ * block name: `PublicUniforms` => editable "uniform", everything else => "host").
+ *
+ * The output matches the "bindings" reflection shape accepted by
+ * `normalizeReflection()` in mxtsladapter.js, so no adapter changes are required.
+ */
+
+// Uniform block names emitted by HwShaderGenerator (see source/MaterialXGenHw/HwConstants.cpp).
+// The role split mirrors the original C++ manifest: PublicUniforms members are the
+// user-editable surface inputs; all other blocks are engine/host-set.
+const PUBLIC_UNIFORMS = 'PublicUniforms';
+const LIGHT_DATA = 'LightData';
+
+// Entry-function names emitted by WgslShaderGenerator::emitPixelStage / emitVertexStage
+// (see source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp). The adapter is entry-name
+// agnostic (it reads manifest.entry), so emitting the in-repo names is sufficient.
+const PIXEL_ENTRY = 'FragmentMain';
+const VERTEX_ENTRY = 'VertexMain';
+
+/**
+ * Format a MaterialX port value as a JS scalar / array / boolean.
+ * Mirrors the C++ `valueToJson` in WgslShaderGenerator.cpp so the produced object graph
+ * is identical to what `JSON.parse(getWgslGeneratedManifest(...))` used to yield.
+ *
+ * @param {Object} port - A bound mx.ShaderPort, or null.
+ * @return {(number|boolean|number[]|null)} The default value, or null when unset.
+ */
+function portValueToJson( port ) {
+
+ const value = port && port.getValue && port.getValue();
+ if ( ! value ) return null;
+
+ const s = value.getValueString();
+ if ( s.indexOf( ',' ) !== - 1 ) {
+
+ // Vector / color / matrix: comma-separated numbers.
+ return s.split( ',' )
+ .map( ( item ) => item.trim() )
+ .filter( ( item ) => item.length > 0 )
+ .map( ( item ) => parseFloat( item ) );
+
+ }
+ if ( s === 'true' ) return true;
+ if ( s === 'false' ) return false;
+ if ( s === '' ) return 0;
+ const n = Number( s );
+ return Number.isNaN( n ) ? s : n;
+
+}
+
+/**
+ * Build a name -> { role, port } map from the pixel stage's uniform blocks.
+ * The block name determines the role; LightData members are not enumerated individually
+ * (the light-data array is exposed as a single `var` binding instead).
+ */
+function collectUniformPorts( shader ) {
+
+ const map = new Map();
+ let stage;
+ try {
+
+ stage = shader.getStage( 'pixel' );
+
+ } catch ( e ) {
+
+ return map;
+
+ }
+ if ( ! stage ) return map;
+
+ const blocks = stage.getUniformBlocks(); // JS object keyed by block name
+ for ( const blockName of Object.keys( blocks ) ) {
+
+ if ( blockName === LIGHT_DATA ) continue;
+ const block = blocks[ blockName ];
+ if ( ! block ) continue;
+ const role = blockName === PUBLIC_UNIFORMS ? 'uniform' : 'host';
+ const count = block.size();
+ for ( let i = 0; i < count; ++ i ) {
+
+ const port = block.get( i );
+ if ( ! port ) continue;
+ map.set( port.getVariable(), { role, port } );
+
+ }
+
+ }
+
+ return map;
+
+}
+
+// Matches `@group(N) @binding(M) var[] name: type;` lines emitted by
+// WgslResourceBindingContext. `var` covers scalar/vector/matrix/array uniforms;
+// bare `var` covers texture_2d and sampler bindings.
+const BINDING_RE = /@group\(\s*(\d+)\s*\)\s*@binding\(\s*(\d+)\s*\)\s*var(?:<[^>]*>)?\s+([A-Za-z_]\w*)\s*:\s*([^;]+);/g;
+
+/**
+ * Parse the `@group/@binding` resource declarations out of the WGSL text into manifest
+ * bindings, attaching role + default value from the uniform-port map.
+ */
+function parseBindings( wgsl, portMap ) {
+
+ const bindings = [];
+ BINDING_RE.lastIndex = 0;
+ for ( let m = BINDING_RE.exec( wgsl ); m !== null; m = BINDING_RE.exec( wgsl ) ) {
+
+ const group = Number( m[ 1 ] );
+ const binding = Number( m[ 2 ] );
+ const name = m[ 3 ];
+ const type = m[ 4 ].trim();
+
+ if ( name.endsWith( '_texture' ) || type.startsWith( 'texture_' ) ) {
+
+ bindings.push( { stage: 'pixel', group, binding, name, type, role: 'texture', key: name.replace( /_texture$/, '' ) } );
+
+ } else if ( name.endsWith( '_sampler' ) || type === 'sampler' ) {
+
+ bindings.push( { stage: 'pixel', group, binding, name, type, role: 'sampler', key: name.replace( /_sampler$/, '' ) } );
+
+ } else if ( type.startsWith( 'array<' ) ) {
+
+ // Light-data array (struct array). The adapter re-parses the struct itself,
+ // so no per-member value is needed here.
+ bindings.push( { stage: 'pixel', group, binding, name, type, role: 'lightData' } );
+
+ } else {
+
+ const info = portMap.get( name );
+ const role = info ? info.role : 'host';
+ const value = info ? portValueToJson( info.port ) : null;
+ bindings.push( { stage: 'pixel', group, binding, name, type, role, value } );
+
+ }
+
+ }
+
+ return bindings;
+
+}
+
+/**
+ * Parse the pixel entry signature `fn FragmentMain(: ) -> vec4f` plus the
+ * referenced `struct { name: type, ... }` into a single entryParam carrying the
+ * varying members. normalizeReflection() flattens this into the varying input list.
+ */
+function parseEntryParams( wgsl ) {
+
+ const sig = new RegExp( `\\bfn\\s+${ PIXEL_ENTRY }\\s*\\(\\s*([A-Za-z_]\\w*)\\s*:\\s*([A-Za-z_]\\w*)\\s*\\)` ).exec( wgsl );
+ if ( ! sig ) return [];
+
+ const instName = sig[ 1 ];
+ const structName = sig[ 2 ];
+
+ const structRe = new RegExp( `\\bstruct\\s+${ structName }\\s*\\{([^}]*)\\}` );
+ const structMatch = structRe.exec( wgsl );
+ const members = [];
+ if ( structMatch ) {
+
+ for ( const rawLine of structMatch[ 1 ].split( '\n' ) ) {
+
+ const line = rawLine.trim().replace( /,$/, '' );
+ if ( ! line ) continue;
+ const colon = line.indexOf( ':' );
+ if ( colon < 0 ) continue;
+ const name = line.slice( 0, colon ).trim();
+ const type = line.slice( colon + 1 ).trim();
+ if ( name && type ) members.push( { name, type } );
+
+ }
+
+ }
+
+ return [ { name: instName, type: structName, members } ];
+
+}
+
+/**
+ * Build the WGSL reflection manifest (bindings format) from a generated Shader + its WGSL.
+ *
+ * @param {Object} shader - The mx.Shader returned by WgslShaderGenerator.generate().
+ * @param {string} wgsl - The generated pixel-stage WGSL source.
+ * @return {Object} Manifest consumable by normalizeReflection() / convertToTslPortable().
+ */
+export function buildWgslManifest( shader, wgsl ) {
+
+ const portMap = collectUniformPorts( shader );
+ return {
+ entry: { vertex: VERTEX_ENTRY, pixel: PIXEL_ENTRY },
+ output: 'vec4f',
+ entryParams: parseEntryParams( wgsl ),
+ bindings: parseBindings( wgsl, portMap )
+ };
+
+}
diff --git a/javascript/MaterialXView/webpack.config.js b/javascript/MaterialXView/webpack.config.js
index b3f0e33f79..b00c0aef53 100644
--- a/javascript/MaterialXView/webpack.config.js
+++ b/javascript/MaterialXView/webpack.config.js
@@ -1,5 +1,6 @@
const path = require('path');
const fs = require('fs');
+const webpack = require('webpack');
const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin')
@@ -11,9 +12,9 @@ function processMaterialPath(materialPath, baseURL) {
const dirent = fs.readdirSync(materialPath).filter(
function (file) { if (file.lastIndexOf(".mtlx") > -1) return file; }
);
- return dirent.map((fileName) => ({
- name: fileName,
- value: `${baseURL}/${fileName}`
+ return dirent.map((fileName) => ({
+ name: fileName,
+ value: `${baseURL}/${fileName}`
}));
}
@@ -32,47 +33,58 @@ dirent = fs.readdirSync(geometryFiles).filter(
let geometry = dirent
.map((fileName) => ({ name: fileName, value: `${geometryFilesURL}/${fileName}` }));
-module.exports = {
- entry: './source/index.js',
- output: {
- filename: 'main.js',
- path: path.resolve(__dirname, 'dist')
- },
- mode: "development",
- plugins: [
- new HtmlWebpackPlugin({
- templateParameters: {
- materials,
- geometry
- },
- template: 'index.ejs'
- }),
- new CopyPlugin({
- patterns: [
- {
- context: "../../resources/Images",
- from: "*.*",
- to: "Images",
- },
- {
- context: "../../resources/Geometry/",
- from: "*.glb",
- to: "Geometry",
- },
- { from: "./public", to: 'public' },
- { context: "../../resources/Lights", from: "*.*", to: "Lights" },
- { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" },
- // Dynamically generate material copy patterns from configuration
- ...materialConfig.materials.map(materialType => ({
- from: materialType.path,
- to: materialType.baseURL
- })),
- { from: "../build/bin/JsMaterialXCore.wasm" },
- { from: "../build/bin/JsMaterialXCore.js" },
- { from: "../build/bin/JsMaterialXGenShader.wasm" },
- { from: "../build/bin/JsMaterialXGenShader.js" },
- { from: "../build/bin/JsMaterialXGenShader.data" },
- ],
- }),
- ]
-};
+// Shared asset copy (WASM, libraries, textures, materials, geometry, lights). Applied to
+// both backend configs so index-webgpu.html deploys standalone with all assets.
+const copyPlugin = new CopyPlugin({
+ patterns: [
+ { context: "../../resources/Images", from: "*.*", to: "Images" },
+ { context: "../../resources/Geometry/", from: "*.glb", to: "Geometry" },
+ { from: "./public", to: 'public' },
+ { context: "../../resources/Lights", from: "*.*", to: "Lights" },
+ { context: "../../resources/Lights/irradiance", from: "*.*", to: "Lights/irradiance" },
+ ...materialConfig.materials.map(materialType => ({
+ from: materialType.path,
+ to: materialType.baseURL
+ })),
+ { from: "../build/bin/JsMaterialXCore.wasm" },
+ { from: "../build/bin/JsMaterialXCore.js" },
+ { from: "../build/bin/JsMaterialXGenShader.wasm" },
+ { from: "../build/bin/JsMaterialXGenShader.js" },
+ { from: "../build/bin/JsMaterialXGenShader.data" },
+ ],
+});
+
+// Build one config per rendering backend. Both compile the SAME source; a `__BACKEND__`
+// define + a per-backend `three` alias select the WebGL (classic THREE.WebGLRenderer +
+// RawShaderMaterial / ESSL) or WebGPU (WebGPURenderer + NodeMaterial / WGSL) path. The two
+// HTML pages let a toggle switch backends by navigation (avoids loading both three builds).
+function makeConfig(backend) {
+ const isWebGPU = backend === 'webgpu';
+ return {
+ name: backend,
+ entry: './source/index.js',
+ output: {
+ filename: isWebGPU ? 'main.webgpu.js' : 'main.js',
+ path: path.resolve(__dirname, 'dist'),
+ },
+ mode: "development",
+ // Each bundle references only its backend's renderer at runtime, but both code paths
+ // exist in the shared source, so the unused one's renderer isn't in this build's three
+ // export set. That's expected — silence the "export not found in 'three'" warning.
+ ignoreWarnings: [ (w) => /was not found in 'three'/.test((w && w.message) || '') ],
+ // For WebGPU, resolve the bare `three` specifier to the WebGPU build (exact match
+ // via `three$`, so `three/tsl` and `three/webgpu` still resolve normally).
+ resolve: isWebGPU ? { alias: { 'three$': require.resolve('three/webgpu') } } : {},
+ plugins: [
+ new webpack.DefinePlugin({ __BACKEND__: JSON.stringify(backend) }),
+ new HtmlWebpackPlugin({
+ filename: isWebGPU ? 'index-webgpu.html' : 'index.html',
+ template: 'index.ejs',
+ templateParameters: { materials, geometry, backend },
+ }),
+ copyPlugin,
+ ],
+ };
+}
+
+module.exports = [makeConfig('webgl'), makeConfig('webgpu')];
diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py
index 46ff359043..e90a69cb43 100644
--- a/python/Scripts/generateshader.py
+++ b/python/Scripts/generateshader.py
@@ -126,12 +126,15 @@ def main():
genoptions.shaderInterfaceType = mx_gen_shader.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE
print('- Set up CMS ...')
- cms = mx_gen_shader.DefaultColorManagementSystem.create(shadergen.getTarget())
+ cmsTarget = shadergen.getTarget()
+ if gentarget == 'wgsl' and cmsTarget not in ('genglsl', 'genmsl', 'genosl'):
+ cmsTarget = 'genglsl'
+ cms = mx_gen_shader.DefaultColorManagementSystem.create(cmsTarget)
cms.loadLibrary(doc)
shadergen.setColorManagementSystem(cms)
print('- Set up Units ...')
- unitsystem = mx_gen_shader.UnitSystem.create(shadergen.getTarget())
+ unitsystem = mx_gen_shader.UnitSystem.create(cmsTarget)
registry = mx.UnitConverterRegistry.create()
distanceTypeDef = doc.getUnitTypeDef('distance')
registry.addUnitConverter(distanceTypeDef, mx.LinearUnitConverter.create(distanceTypeDef))
diff --git a/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp
index cac1d91e47..5b34c12323 100644
--- a/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp
+++ b/source/JsMaterialX/JsMaterialXGenEssl/JsEsslShaderGenerator.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
#include
#include
diff --git a/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp
index e55b64a397..1a5bf49676 100644
--- a/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp
+++ b/source/JsMaterialX/JsMaterialXGenVk/JsVkShaderGenerator.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
#include
#include
diff --git a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp
index b0c05d3228..b54372bc5d 100644
--- a/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp
+++ b/source/JsMaterialX/JsMaterialXGenWgsl/JsWgslShaderGenerator.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
#include
#include
diff --git a/source/MaterialXGenGlsl/CMakeLists.txt b/source/MaterialXGenGlsl/CMakeLists.txt
index 8a9e4acdeb..838fc83d51 100644
--- a/source/MaterialXGenGlsl/CMakeLists.txt
+++ b/source/MaterialXGenGlsl/CMakeLists.txt
@@ -1,5 +1,6 @@
file(GLOB_RECURSE materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
file(GLOB_RECURSE materialx_headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h*")
+list(FILTER materialx_headers EXCLUDE REGEX "wgsl/converter/") # internal WGSL helpers; not installed
mx_add_library(MaterialXGenGlsl
SOURCE_FILES
diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp b/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp
deleted file mode 100644
index b9ceb270a4..0000000000
--- a/source/MaterialXGenGlsl/WgslResourceBindingContext.cpp
+++ /dev/null
@@ -1,105 +0,0 @@
-//
-// Copyright Contributors to the MaterialX Project
-// SPDX-License-Identifier: Apache-2.0
-//
-
-#include
-
-#include
-#include
-
-MATERIALX_NAMESPACE_BEGIN
-
-//
-// WgslResourceBindingContext methods
-//
-
-WgslResourceBindingContext::WgslResourceBindingContext(size_t uniformBindingLocation) :
- VkResourceBindingContext(uniformBindingLocation)
-{
-}
-
-// Copied from VkResourceBindingContext::emitResourceBindings().
-// Modified the Type::FILENAME uniform codegen.
-void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage)
-{
- const ShaderGenerator& generator = context.getShaderGenerator();
- const Syntax& syntax = generator.getSyntax();
-
- // First, emit all value uniforms in a block with single layout binding
- bool hasValueUniforms = false;
- for (auto uniform : uniforms.getVariableOrder())
- {
- if (uniform->getType() != Type::FILENAME)
- {
- hasValueUniforms = true;
- break;
- }
- }
- if (hasValueUniforms)
- {
- generator.emitLine("layout (std140, binding=" + std::to_string(_hwUniformBindLocation++) + ") " +
- syntax.getUniformQualifier() + " " + uniforms.getName() + "_" + stage.getName(),
- stage, false);
- generator.emitScopeBegin(stage);
- for (auto uniform : uniforms.getVariableOrder())
- {
- if (uniform->getType() != Type::FILENAME)
- {
- if ( uniform->getType() == Type::BOOLEAN )
- {
- // Cannot have boolean uniforms in WGSL
- std::cerr << "Warning: WGSL does not allow boolean types to be stored in uniform or storage address spaces." << std::endl;
-
- // Set uniform type to integer
- uniform->setType( Type::INTEGER );
-
- // Write declaration as normal
- generator.emitLineBegin(stage);
- generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false);
- generator.emitString(Syntax::SEMICOLON, stage);
- generator.emitLineEnd(stage, false);
-
- // Add macro to treat any follow usages of this variable as a boolean
- // eg. u_myUniformBool -> bool(u_myUniformBool)
- generator.emitString("#define " + uniform->getVariable() + " bool(" + uniform->getVariable() + ")", stage);
- generator.emitLineBreak(stage);
- }
- else
- {
- generator.emitLineBegin(stage);
- generator.emitVariableDeclaration(uniform, EMPTY_STRING, context, stage, false);
- generator.emitString(Syntax::SEMICOLON, stage);
- generator.emitLineEnd(stage, false);
- }
-
- }
- }
- generator.emitScopeEnd(stage, true);
- }
-
- // Second, emit all sampler uniforms as separate uniforms with separate layout bindings
- for (auto uniform : uniforms.getVariableOrder())
- {
- if (uniform->getType() == Type::FILENAME)
- {
- // Bind separately as texture2D + sampler
- //
- // NOTE: the *_texture and *_sampler binding names method below expect that
- // variables from HwShaderGenerator.cpp (HW::ENV_RADIANCE_SPLIT and HW::ENV_IRRADIANCE_SPLIT)
- // use the same naming convention as here.
- //
- generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage);
- generator.emitString(string("texture2D ")+uniform->getVariable()+"_texture", stage);
- generator.emitLineEnd(stage, true);
-
- generator.emitString("layout (binding=" + std::to_string(_hwUniformBindLocation++) + ") " + syntax.getUniformQualifier() + " ", stage);
- generator.emitString(string("sampler ")+uniform->getVariable()+"_sampler", stage);
- generator.emitLineEnd(stage, true);
- }
- }
-
- generator.emitLineBreak(stage);
-}
-
-MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp b/source/MaterialXGenGlsl/WgslShaderGenerator.cpp
deleted file mode 100644
index d39a2498f4..0000000000
--- a/source/MaterialXGenGlsl/WgslShaderGenerator.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-//
-// Copyright Contributors to the MaterialX Project
-// SPDX-License-Identifier: Apache-2.0
-//
-
-#include
-#include
-
-#include
-
-MATERIALX_NAMESPACE_BEGIN
-
-const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type";
-
-WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) :
- VkShaderGenerator(typeSystem)
-{
- _syntax = WgslSyntax::create(typeSystem);
-
- // Set binding context to handle resource binding layouts
- _resourceBindingCtx = std::make_shared(0);
-
- // For functions described in ::emitSpecularEnvironment()
- // override map value from HwShaderGenerator
- _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE_SPLIT;
- _tokenSubstitutions[HW::T_ENV_RADIANCE_SAMPLER2D] = HW::ENV_RADIANCE_SAMPLER2D_SPLIT;
- _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE_SPLIT;
- _tokenSubstitutions[HW::T_ENV_IRRADIANCE_SAMPLER2D] = HW::ENV_IRRADIANCE_SAMPLER2D_SPLIT;
- _tokenSubstitutions[HW::T_TEX_SAMPLER_SAMPLER2D] = HW::TEX_SAMPLER_SAMPLER2D_SPLIT;
- _tokenSubstitutions[HW::T_TEX_SAMPLER_SIGNATURE] = HW::TEX_SAMPLER_SIGNATURE_SPLIT;
-}
-
-void WgslShaderGenerator::emitDirectives(GenContext& context, ShaderStage& stage) const
-{
- VkShaderGenerator::emitDirectives(context, stage);
- // Add additional directives and #define statements here
- // Example: emitLine("#define HW_SEPARATE_SAMPLERS", stage, false);
- emitLineBreak(stage);
-}
-
-// Called by CompoundNode::emitFunctionDefinition()
-void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const
-{
- if (shaderPort->getType() == Type::FILENAME)
- {
- emitString("texture2D " + shaderPort->getVariable() + "_texture, sampler "+shaderPort->getVariable() + "_sampler", stage);
- }
- else
- {
- VkShaderGenerator::emitFunctionDefinitionParameter(shaderPort, isOutput, context, stage);
- }
-}
-
-// Called by SourceCodeNode::emitFunctionCall()
-void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const
-{
- if (input->getType() == Type::FILENAME)
- {
- emitString(getUpstreamResult(input, context)+"_texture, "+getUpstreamResult(input, context)+"_sampler", stage);
- }
- else
- {
- VkShaderGenerator::emitInput(input, context, stage);
- }
-}
-
-MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXGenGlsl/WgslShaderGenerator.h b/source/MaterialXGenGlsl/WgslShaderGenerator.h
deleted file mode 100644
index af9141f440..0000000000
--- a/source/MaterialXGenGlsl/WgslShaderGenerator.h
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-// Copyright Contributors to the MaterialX Project
-// SPDX-License-Identifier: Apache-2.0
-//
-
-#ifndef MATERIALX_WGSLSHADERGENERATOR_H
-#define MATERIALX_WGSLSHADERGENERATOR_H
-
-/// @file
-/// Vulkan GLSL shader generator flavor for WGSL
-
-#include
-#include
-#include
-
-MATERIALX_NAMESPACE_BEGIN
-
-using WgslShaderGeneratorPtr = shared_ptr;
-
-/// @class WgslShaderGenerator
-/// WGSL Flavor of Vulkan GLSL shader generator
-class MX_GENGLSL_API WgslShaderGenerator : public VkShaderGenerator
-{
- public:
- /// Constructor.
- WgslShaderGenerator(TypeSystemPtr typeSystem);
-
- /// Creator function.
- /// If a TypeSystem is not provided it will be created internally.
- /// Optionally pass in an externally created TypeSystem here,
- /// if you want to keep type descriptions alive after the lifetime
- /// of the shader generator.
- static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr)
- {
- return std::make_shared(typeSystem ? typeSystem : TypeSystem::create());
- }
-
- void emitDirectives(GenContext& context, ShaderStage& stage) const override;
-
- const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; }
-
- void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override;
-
- void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override;
-
- protected:
- static const string LIGHTDATA_TYPEVAR_STRING;
-};
-
-MATERIALX_NAMESPACE_END
-
-#endif
diff --git a/source/MaterialXGenGlsl/EsslShaderGenerator.cpp b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp
similarity index 97%
rename from source/MaterialXGenGlsl/EsslShaderGenerator.cpp
rename to source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp
index 8c074ea555..45071fe7e7 100644
--- a/source/MaterialXGenGlsl/EsslShaderGenerator.cpp
+++ b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.cpp
@@ -3,8 +3,8 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
-#include
+#include
+#include
#include
#include
diff --git a/source/MaterialXGenGlsl/EsslShaderGenerator.h b/source/MaterialXGenGlsl/essl/EsslShaderGenerator.h
similarity index 100%
rename from source/MaterialXGenGlsl/EsslShaderGenerator.h
rename to source/MaterialXGenGlsl/essl/EsslShaderGenerator.h
diff --git a/source/MaterialXGenGlsl/EsslSyntax.cpp b/source/MaterialXGenGlsl/essl/EsslSyntax.cpp
similarity index 87%
rename from source/MaterialXGenGlsl/EsslSyntax.cpp
rename to source/MaterialXGenGlsl/essl/EsslSyntax.cpp
index fb391645af..7ab3d9e490 100644
--- a/source/MaterialXGenGlsl/EsslSyntax.cpp
+++ b/source/MaterialXGenGlsl/essl/EsslSyntax.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
#include
#include
diff --git a/source/MaterialXGenGlsl/EsslSyntax.h b/source/MaterialXGenGlsl/essl/EsslSyntax.h
similarity index 100%
rename from source/MaterialXGenGlsl/EsslSyntax.h
rename to source/MaterialXGenGlsl/essl/EsslSyntax.h
diff --git a/source/MaterialXGenGlsl/VkResourceBindingContext.cpp b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp
similarity index 99%
rename from source/MaterialXGenGlsl/VkResourceBindingContext.cpp
rename to source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp
index 692334f120..5a262061f8 100644
--- a/source/MaterialXGenGlsl/VkResourceBindingContext.cpp
+++ b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
#include
diff --git a/source/MaterialXGenGlsl/VkResourceBindingContext.h b/source/MaterialXGenGlsl/vk/VkResourceBindingContext.h
similarity index 100%
rename from source/MaterialXGenGlsl/VkResourceBindingContext.h
rename to source/MaterialXGenGlsl/vk/VkResourceBindingContext.h
diff --git a/source/MaterialXGenGlsl/VkShaderGenerator.cpp b/source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp
similarity index 97%
rename from source/MaterialXGenGlsl/VkShaderGenerator.cpp
rename to source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp
index 22ff1f85c2..0c4c461c05 100644
--- a/source/MaterialXGenGlsl/VkShaderGenerator.cpp
+++ b/source/MaterialXGenGlsl/vk/VkShaderGenerator.cpp
@@ -3,8 +3,8 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
-#include
+#include
+#include
#include
MATERIALX_NAMESPACE_BEGIN
diff --git a/source/MaterialXGenGlsl/VkShaderGenerator.h b/source/MaterialXGenGlsl/vk/VkShaderGenerator.h
similarity index 97%
rename from source/MaterialXGenGlsl/VkShaderGenerator.h
rename to source/MaterialXGenGlsl/vk/VkShaderGenerator.h
index 45647af3e7..bec06fad8c 100644
--- a/source/MaterialXGenGlsl/VkShaderGenerator.h
+++ b/source/MaterialXGenGlsl/vk/VkShaderGenerator.h
@@ -10,7 +10,7 @@
/// Vulkan GLSL shader generator
#include
-#include
+#include
MATERIALX_NAMESPACE_BEGIN
diff --git a/source/MaterialXGenGlsl/VkSyntax.cpp b/source/MaterialXGenGlsl/vk/VkSyntax.cpp
similarity index 84%
rename from source/MaterialXGenGlsl/VkSyntax.cpp
rename to source/MaterialXGenGlsl/vk/VkSyntax.cpp
index 563190ad51..35d2fcd188 100644
--- a/source/MaterialXGenGlsl/VkSyntax.cpp
+++ b/source/MaterialXGenGlsl/vk/VkSyntax.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
MATERIALX_NAMESPACE_BEGIN
diff --git a/source/MaterialXGenGlsl/VkSyntax.h b/source/MaterialXGenGlsl/vk/VkSyntax.h
similarity index 100%
rename from source/MaterialXGenGlsl/VkSyntax.h
rename to source/MaterialXGenGlsl/vk/VkSyntax.h
diff --git a/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp
new file mode 100644
index 0000000000..85496eab34
--- /dev/null
+++ b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.cpp
@@ -0,0 +1,114 @@
+//
+// Copyright Contributors to the MaterialX Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include
+
+#include
+
+#include
+
+MATERIALX_NAMESPACE_BEGIN
+
+//
+// WgslResourceBindingContext methods
+//
+
+WgslResourceBindingContext::WgslResourceBindingContext(size_t uniformBindingLocation) :
+ VkResourceBindingContext(uniformBindingLocation)
+{
+}
+
+void WgslResourceBindingContext::emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage)
+{
+ const ShaderGenerator& generator = context.getShaderGenerator();
+ const Syntax& syntax = generator.getSyntax();
+
+ for (auto uniform : uniforms.getVariableOrder())
+ {
+ const TypeDesc t = uniform->getType();
+ if (t.isClosure() || t == Type::SURFACESHADER || t == Type::MATERIAL ||
+ t == Type::DISPLACEMENTSHADER || t == Type::VOLUMESHADER || t == Type::LIGHTSHADER)
+ {
+ continue;
+ }
+
+ if (uniform->getType() == Type::FILENAME)
+ {
+ const string& name = uniform->getVariable();
+ // Bind separately as texture_2d + sampler.
+ //
+ // NOTE: the *_texture and *_sampler binding names below expect that
+ // variables from HwShaderGenerator.cpp (HW::ENV_RADIANCE_SPLIT and HW::ENV_IRRADIANCE_SPLIT)
+ // use the same naming convention as here.
+ //
+ generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + name +
+ "_texture: texture_2d",
+ stage);
+ generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " + name +
+ "_sampler: sampler",
+ stage);
+ }
+ else
+ {
+ const string typeName = getWgslUniformType(uniform, syntax);
+ generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " +
+ uniform->getVariable() + ": " + typeName,
+ stage);
+ }
+ }
+
+ generator.emitLineBreak(stage);
+}
+
+// Emit a uniform block as a WGSL struct plus a single `var` binding for it.
+// Used for the light-data array (a scalar suffix like "[4]" becomes a WGSL `array`).
+void WgslResourceBindingContext::emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms,
+ ShaderStage& stage, const std::string& structInstanceName,
+ const std::string& arraySuffix)
+{
+ const ShaderGenerator& generator = context.getShaderGenerator();
+ const Syntax& syntax = generator.getSyntax();
+
+ // Emit a WGSL struct for the uniform block members.
+ generator.emitLine("struct " + uniforms.getName(), stage, false);
+ generator.emitScopeBegin(stage);
+ for (size_t i = 0; i < uniforms.size(); ++i)
+ {
+ const string typeName = getWgslUniformType(uniforms[i], syntax);
+ // WGSL struct members are comma-separated (not `;`-terminated like GLSL).
+ generator.emitLine(uniforms[i]->getVariable() + ": " + typeName + ",", stage, false);
+ }
+ generator.emitScopeEnd(stage, false);
+
+ if (!arraySuffix.empty())
+ {
+ // arraySuffix is e.g. "[4]" — extract count for WGSL array<> syntax.
+ const string count = arraySuffix.substr(1, arraySuffix.size() - 2);
+ generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " +
+ structInstanceName + ": array<" + uniforms.getName() + ", " + count + ">",
+ stage);
+ }
+ else
+ {
+ generator.emitLine("@group(0) @binding(" + std::to_string(_hwUniformBindLocation++) + ") var " +
+ structInstanceName + ": " + uniforms.getName(),
+ stage);
+ }
+ generator.emitLineBreak(stage);
+}
+
+// Map a uniform port's type to its WGSL spelling: booleans become i32 (WGSL forbids bool
+// in uniform/storage address spaces), and every other GLSL type name is converted to WGSL.
+string WgslResourceBindingContext::getWgslUniformType(const ShaderPort* port, const Syntax& syntax) const
+{
+ if (port->getType() == Type::BOOLEAN)
+ {
+ // WGSL does not allow boolean types in uniform or storage address spaces.
+ return "i32";
+ }
+ return GlslToWgsl::mapType(syntax.getTypeName(port->getType()));
+}
+
+MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXGenGlsl/WgslResourceBindingContext.h b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h
similarity index 50%
rename from source/MaterialXGenGlsl/WgslResourceBindingContext.h
rename to source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h
index 7b39b65661..d03a570c30 100644
--- a/source/MaterialXGenGlsl/WgslResourceBindingContext.h
+++ b/source/MaterialXGenGlsl/wgsl/WgslResourceBindingContext.h
@@ -7,11 +7,14 @@
#define MATERIALX_WGSLRESOURCEBINDING_H
/// @file
-/// Vulkan GLSL resource binding context for WGSL
+/// WebGPU WGSL resource binding context
#include
-#include
+#include
+
+#include
+#include
MATERIALX_NAMESPACE_BEGIN
@@ -19,7 +22,7 @@ MATERIALX_NAMESPACE_BEGIN
using WgslResourceBindingContextPtr = shared_ptr;
/// @class WgslResourceBindingContext
-/// Class representing a resource binding for Vulkan Glsl shader resources.
+/// Class representing a resource binding for WGSL shader resources.
class MX_GENGLSL_API WgslResourceBindingContext : public VkResourceBindingContext
{
public:
@@ -32,6 +35,19 @@ class MX_GENGLSL_API WgslResourceBindingContext : public VkResourceBindingContex
// Emit uniforms with binding information
void emitResourceBindings(GenContext& context, const VariableBlock& uniforms, ShaderStage& stage) override;
+
+ // Emit structured uniforms with binding information
+ void emitStructuredResourceBindings(GenContext& context, const VariableBlock& uniforms,
+ ShaderStage& stage, const std::string& structInstanceName,
+ const std::string& arraySuffix) override;
+
+ // Current @binding index (advanced by emitResourceBindings)
+ size_t getBindingLocation() const { return _hwUniformBindLocation; }
+
+ void setBindingLocation(size_t location) { _hwUniformBindLocation = location; }
+
+ // Emit a WGSL type name for a uniform port (maps bool to i32)
+ string getWgslUniformType(const ShaderPort* port, const Syntax& syntax) const;
};
MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp
new file mode 100644
index 0000000000..de17d18722
--- /dev/null
+++ b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.cpp
@@ -0,0 +1,740 @@
+//
+// Copyright Contributors to the MaterialX Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+MATERIALX_NAMESPACE_BEGIN
+
+const string WgslShaderGenerator::TARGET = "genwgsl";
+const string WgslShaderGenerator::LIGHTDATA_TYPEVAR_STRING = "light_type";
+
+namespace
+{
+// Tracks include files (by basename) already expanded during a generation, so the
+// genglsl headers (closure types, math) that many BSDF sources #include are emitted
+// once rather than producing duplicate WGSL struct/const definitions.
+class WgslIncludeSet : public GenUserData
+{
+ public:
+ std::set basenames;
+};
+const string WGSL_INCLUDES = "WGSL_INCLUDES";
+
+bool isIdentChar(char c)
+{
+ return std::isalnum(static_cast(c)) || c == '_';
+}
+
+string getWgslTypeName(const Syntax& syntax, const TypeDesc& type)
+{
+ if (type == Type::BOOLEAN)
+ return "i32";
+ return GlslToWgsl::mapType(syntax.getTypeName(type));
+}
+
+string mapGlslValueExpr(string value)
+{
+ if (value == "true")
+ return "1";
+ if (value == "false")
+ return "0";
+
+ struct CtorMap
+ {
+ const char* from;
+ const char* to;
+ };
+ static const CtorMap CTORS[] = {
+ { "vec2(", "vec2f(" },
+ { "vec3(", "vec3f(" },
+ { "vec4(", "vec4f(" },
+ { "mat2(", "mat2x2f(" },
+ { "mat3(", "mat3x3f(" },
+ { "mat4(", "mat4x4f(" },
+ };
+ for (const CtorMap& c : CTORS)
+ {
+ const string from = c.from;
+ size_t pos = 0;
+ while ((pos = value.find(from, pos)) != string::npos)
+ {
+ if (pos > 0 && isIdentChar(value[pos - 1]))
+ {
+ pos += from.size();
+ continue;
+ }
+ value.replace(pos, from.size(), c.to);
+ pos += std::strlen(c.to);
+ }
+ }
+ return value;
+}
+
+string getWgslValue(const Syntax& syntax, const TypeDesc& type, const Value& value)
+{
+ if (type == Type::BOOLEAN)
+ return value.getValueString() == "true" ? "1" : "0";
+ return mapGlslValueExpr(syntax.getValue(type, value));
+}
+
+string getWgslDefaultValue(const Syntax& syntax, const TypeDesc& type)
+{
+ if (type == Type::BOOLEAN)
+ return "0";
+ return mapGlslValueExpr(syntax.getDefaultValue(type));
+}
+} // namespace
+
+WgslShaderGenerator::WgslShaderGenerator(TypeSystemPtr typeSystem) :
+ VkShaderGenerator(typeSystem)
+{
+ _syntax = WgslSyntax::create(typeSystem);
+
+ // Set binding context to handle resource binding layouts
+ _resourceBindingCtx = std::make_shared(0);
+
+ // For functions described in ::emitSpecularEnvironment()
+ // override map value from HwShaderGenerator
+ _tokenSubstitutions[HW::T_ENV_RADIANCE] = HW::ENV_RADIANCE_SPLIT;
+ _tokenSubstitutions[HW::T_ENV_RADIANCE_SAMPLER2D] = HW::ENV_RADIANCE_SAMPLER2D_SPLIT;
+ _tokenSubstitutions[HW::T_ENV_IRRADIANCE] = HW::ENV_IRRADIANCE_SPLIT;
+ _tokenSubstitutions[HW::T_ENV_IRRADIANCE_SAMPLER2D] = HW::ENV_IRRADIANCE_SAMPLER2D_SPLIT;
+ _tokenSubstitutions[HW::T_TEX_SAMPLER_SAMPLER2D] = HW::TEX_SAMPLER_SAMPLER2D_SPLIT;
+ _tokenSubstitutions[HW::T_TEX_SAMPLER_SIGNATURE] = HW::TEX_SAMPLER_SIGNATURE_SPLIT;
+}
+
+ShaderNodeImplPtr WgslShaderGenerator::getImplementation(const NodeDef& nodedef, GenContext& context) const
+{
+ InterfaceElementPtr implElement = nodedef.getImplementation(TARGET);
+ if (!implElement)
+ {
+ // WGSL reuses genglsl node implementations until genwgsl-specific overrides exist.
+ implElement = nodedef.getImplementation(VkShaderGenerator::TARGET);
+ }
+ if (!implElement)
+ {
+ return nullptr;
+ }
+
+ const string& name = implElement->getName();
+ ShaderNodeImplPtr impl = context.findNodeImplementation(name);
+ if (impl)
+ {
+ return impl;
+ }
+
+ if (implElement->isA())
+ {
+ impl = createShaderNodeImplForNodeGraph(*implElement->asA());
+ }
+ else if (implElement->isA())
+ {
+ if (getColorManagementSystem() && getColorManagementSystem()->hasImplementation(name))
+ {
+ impl = getColorManagementSystem()->createImplementation(name);
+ }
+ else
+ {
+ impl = _implFactory.create(name);
+ }
+ if (!impl)
+ {
+ impl = createShaderNodeImplForImplementation(*implElement->asA());
+ }
+ }
+ if (!impl)
+ {
+ return nullptr;
+ }
+
+ impl->initialize(*implElement, context);
+ context.addNodeImplementation(name, impl);
+ return impl;
+}
+
+void WgslShaderGenerator::emitVariableDeclaration(const ShaderPort* variable, const string& qualifier,
+ GenContext&, ShaderStage& stage, bool assignValue) const
+{
+ if (variable->getType() == Type::FILENAME)
+ {
+ // Combined GLSL sampler2D becomes a WGSL texture (sampler emitted separately).
+ emitString("var " + variable->getVariable() + ": texture_2d", stage);
+ return;
+ }
+
+ string typeName = getWgslTypeName(*_syntax, variable->getType());
+ if (variable->getType().isArray() && variable->getValue())
+ typeName += _syntax->getArrayVariableSuffix(variable->getType(), *variable->getValue());
+
+ const bool isConst = (qualifier == _syntax->getConstantQualifier());
+ const string keyword = isConst ? "const " : "var ";
+ string line = keyword + variable->getVariable() + ": " + typeName;
+
+ const bool isUniform = (qualifier == _syntax->getUniformQualifier());
+ if (assignValue && !isUniform)
+ {
+ const string valueStr = variable->getValue()
+ ? getWgslValue(*_syntax, variable->getType(), *variable->getValue())
+ : getWgslDefaultValue(*_syntax, variable->getType());
+ if (!valueStr.empty())
+ line += " = " + valueStr;
+ }
+
+ emitString(line, stage);
+}
+
+// Called by CompoundNode::emitFunctionDefinition()
+void WgslShaderGenerator::emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& /*context*/, ShaderStage& stage) const
+{
+ const string& name = shaderPort->getVariable();
+ if (shaderPort->getType() == Type::FILENAME)
+ {
+ VkShaderGenerator::emitString(name + "_texture: texture_2d, " + name + "_sampler: sampler", stage);
+ return;
+ }
+ const string typeName = getWgslTypeName(*_syntax, shaderPort->getType());
+ if (isOutput)
+ {
+ VkShaderGenerator::emitString(name + ": ptr", stage);
+ }
+ else
+ {
+ VkShaderGenerator::emitString(name + ": " + typeName, stage);
+ }
+}
+
+// Called by SourceCodeNode::emitFunctionCall()
+void WgslShaderGenerator::emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const
+{
+ if (input->getType() == Type::FILENAME)
+ {
+ emitString(getUpstreamResult(input, context) + "_texture, " + getUpstreamResult(input, context) + "_sampler", stage);
+ }
+ else
+ {
+ VkShaderGenerator::emitInput(input, context, stage);
+ }
+}
+
+void WgslShaderGenerator::emitOutput(const ShaderOutput* output, bool includeType, bool assignValue,
+ GenContext& context, ShaderStage& stage) const
+{
+ if (includeType)
+ {
+ stage.addString("var " + output->getVariable() + ": " + getWgslTypeName(*_syntax, output->getType()));
+ }
+ else
+ {
+ // Function-call argument: WGSL out parameters are `ptr`, passed as `&var`.
+ stage.addString((assignValue ? "" : "&") + output->getVariable());
+ }
+
+ string suffix;
+ context.getOutputSuffix(output, suffix);
+ if (!suffix.empty())
+ stage.addString(suffix);
+
+ if (assignValue)
+ {
+ const string value = getWgslDefaultValue(*_syntax, output->getType());
+ if (!value.empty())
+ stage.addString(" = " + value);
+ }
+}
+
+void WgslShaderGenerator::emitString(const string& str, ShaderStage& stage) const
+{
+ if (str.size() >= 5 && str.compare(0, 5, "void ") == 0)
+ {
+ string replaced = str;
+ replaced.replace(0, 5, "fn ");
+ VkShaderGenerator::emitString(replaced, stage);
+ return;
+ }
+ VkShaderGenerator::emitString(str, stage);
+}
+
+void WgslShaderGenerator::emitLine(const string& str, ShaderStage& stage, bool semicolon) const
+{
+ // Split a run of statements onto separate lines, then apply the GLSL->WGSL rewrites.
+ const size_t firstNewline = str.find('\n');
+ if (firstNewline != string::npos)
+ {
+ emitLine(str.substr(0, firstNewline), stage, true);
+ emitLine(str.substr(firstNewline + 1), stage, semicolon);
+ return;
+ }
+ VkShaderGenerator::emitLine(GlslToWgsl::rewriteAll(str), stage, semicolon);
+}
+
+void WgslShaderGenerator::emitLineEnd(ShaderStage& stage, bool semicolon) const
+{
+ // Rewrite the line just built via emitString (e.g. by SourceCodeNode) to WGSL.
+ const string& code = stage.getSourceCode();
+ const string& newlineStr = _syntax->getNewline();
+ const size_t newlineLen = newlineStr.empty() ? 1 : newlineStr.size();
+ const size_t lastNewline = code.rfind(newlineStr);
+ const string lastLine = (lastNewline == string::npos) ? code : code.substr(lastNewline + newlineLen);
+ if (!lastLine.empty())
+ {
+ const string rewritten = GlslToWgsl::rewriteAll(lastLine);
+ if (rewritten != lastLine)
+ {
+ const string newCode = (lastNewline == string::npos)
+ ? rewritten
+ : code.substr(0, lastNewline + newlineLen) + rewritten;
+ stage.setSourceCode(newCode);
+ }
+ }
+ VkShaderGenerator::emitLineEnd(stage, semicolon);
+}
+
+void WgslShaderGenerator::emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const
+{
+ if (sourceFilename.getExtension() == "glsl" && !str.empty())
+ {
+ GlslToWgsl::LineRewriter rewriter;
+ const string wgsl = expandAndRewriteGlsl(str, sourceFilename, context, rewriter);
+ stage.addBlock(wgsl, sourceFilename, context);
+ return;
+ }
+ VkShaderGenerator::emitBlock(str, sourceFilename, context, stage);
+}
+
+void WgslShaderGenerator::emitLibraryInclude(const FilePath& filename, GenContext& context, ShaderStage& stage) const
+{
+ FilePath libraryPrefix = context.getOptions().libraryPrefix;
+ FilePath fullFilename = libraryPrefix.isEmpty() ? filename : libraryPrefix / filename;
+ FilePath resolvedFilename = context.resolveSourceFile(fullFilename, FilePath());
+ if (resolvedFilename.exists())
+ {
+ emitBlock(readFile(resolvedFilename), resolvedFilename, context, stage);
+ }
+}
+
+void WgslShaderGenerator::emitTypeDefinitions(GenContext&, ShaderStage&) const
+{
+ // GLSL typedefs are `#define` directives (e.g. `#define EDF vec3`). WGSL has no
+ // preprocessor — surface/closure aliases are emitted by emitWgslSurfaceTypes().
+}
+
+void WgslShaderGenerator::emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const
+{
+ emitDirectives(context, stage);
+ emitLineBreak(stage);
+
+ emitConstants(context, stage);
+ emitUniforms(context, stage);
+ emitInputs(context, stage);
+ emitOutputs(context, stage);
+
+ emitLibraryInclude("stdlib/genglsl/lib/mx_math.glsl", context, stage);
+ emitLineBreak(stage);
+
+ emitFunctionDefinitions(graph, context, stage);
+
+ const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA);
+ const string vdInstance = vertexData.getInstance();
+
+ setFunctionName("VertexMain", stage);
+ emitLine("fn VertexMain(inputs: VertexInput) -> VertexOutput", stage, false);
+ emitFunctionBodyBegin(graph, context, stage);
+
+ emitLine("var output: VertexOutput", stage);
+ emitLine("var " + vdInstance + ": " + vertexData.getName(), stage);
+ emitLine("let hPositionWorld = " + HW::T_WORLD_MATRIX + " * vec4f(inputs." + HW::T_IN_POSITION + ", 1.0)", stage);
+ emitLine("output.position = " + HW::T_VIEW_PROJECTION_MATRIX + " * hPositionWorld", stage);
+
+ for (const ShaderNode* node : graph.getNodes())
+ emitFunctionCall(*node, context, stage);
+
+ emitLine("output." + vdInstance + " = " + vdInstance, stage);
+ emitLine("return output", stage);
+ emitFunctionBodyEnd(graph, context, stage);
+
+ finalizeStageSource(stage);
+}
+
+void WgslShaderGenerator::emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const
+{
+ _tokenSubstitutions[ShaderGenerator::T_FILE_TRANSFORM_UV] =
+ context.getOptions().fileTextureVerticalFlip ? "mx_transform_uv_vflip.glsl" : "mx_transform_uv.glsl";
+
+ auto includeSet = std::make_shared();
+ context.pushUserData(WGSL_INCLUDES, includeSet);
+
+ emitDirectives(context, stage);
+ emitLineBreak(stage);
+
+ const bool lighting = requiresLighting(graph);
+ const ShaderGraphOutputSocket* outputSocket = graph.getOutputSocket();
+ const TypeDesc outSocketType = outputSocket->getType();
+ const bool isSurface = lighting || outSocketType == Type::SURFACESHADER || outSocketType == Type::MATERIAL;
+
+ if (isSurface)
+ emitWgslSurfaceTypes(context, stage);
+
+ emitTypeDefinitions(context, stage);
+ emitConstants(context, stage);
+
+ emitPixelUniforms(context, stage, lighting);
+
+ const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA);
+ emitVertexDataStruct(vertexData, stage);
+
+ emitLibraryInclude("stdlib/genglsl/lib/mx_math.glsl", context, stage);
+ emitLineBreak(stage);
+
+ if (lighting || context.getOptions().hwWriteAlbedoTable || context.getOptions().hwWriteEnvPrefilter)
+ {
+ emitLine("const DIRECTIONAL_ALBEDO_METHOD: i32 = " + std::to_string(int(context.getOptions().hwDirectionalAlbedoMethod)), stage);
+ emitLineBreak(stage);
+ }
+
+ emitLine("const AIRY_FRESNEL_ITERATIONS: i32 = " + std::to_string(context.getOptions().hwAiryFresnelIterations), stage);
+ emitLineBreak(stage);
+
+ if (lighting)
+ {
+ if (context.getOptions().hwMaxActiveLightSources > 0)
+ {
+ const unsigned int maxLights = std::max(1u, context.getOptions().hwMaxActiveLightSources);
+ emitLine("const " + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + ": i32 = " + std::to_string(maxLights), stage);
+ emitLineBreak(stage);
+ }
+ emitSpecularEnvironment(context, stage);
+ emitTransmissionRender(context, stage);
+ if (context.getOptions().hwMaxActiveLightSources > 0)
+ emitLightData(context, stage);
+ }
+
+ emitLightFunctionDefinitions(graph, context, stage);
+ emitFunctionDefinitions(graph, context, stage);
+ emitLineBreak(stage);
+
+ const string vdInstance = vertexData.getInstance();
+ const string entryParam = vdInstance + ": " + vertexData.getName();
+ setFunctionName("FragmentMain", stage);
+ emitLine("fn FragmentMain(" + entryParam + ") -> vec4f", stage, false);
+ emitScopeBegin(stage);
+
+ if (graph.hasClassification(ShaderNode::Classification::SHADER | ShaderNode::Classification::SURFACE))
+ {
+ emitFunctionCalls(graph, context, stage, ShaderNode::Classification::TEXTURE);
+ for (const ShaderGraphOutputSocket* socket : graph.getOutputSockets())
+ {
+ if (socket->getConnection())
+ {
+ const ShaderNode* upstream = socket->getConnection()->getNode();
+ if (upstream->getParent() == &graph &&
+ (upstream->hasClassification(ShaderNode::Classification::CLOSURE) ||
+ upstream->hasClassification(ShaderNode::Classification::SHADER)))
+ {
+ emitFunctionCall(*upstream, context, stage);
+ }
+ }
+ }
+ }
+ else
+ {
+ emitFunctionCalls(graph, context, stage);
+ }
+
+ const ShaderOutput* outputConnection = outputSocket->getConnection();
+ string outValue = outputConnection ? outputConnection->getVariable() : getWgslDefaultValue(*_syntax, Type::COLOR3);
+ const TypeDesc outType = outputSocket->getType();
+ string vec4Value;
+ if (isSurface && outputConnection)
+ {
+ string outColor = outValue + ".color";
+ const string outTransparency = outValue + ".transparency";
+ if (context.getOptions().hwSrgbEncodeOutput)
+ outColor = "mx_srgb_encode(" + outColor + ")";
+ if (context.getOptions().hwTransparency)
+ {
+ emitLine("let outAlpha: f32 = clamp(1.0 - dot(" + outTransparency + ", vec3f(0.3333)), 0.0, 1.0)", stage);
+ vec4Value = "vec4f(" + outColor + ", outAlpha)";
+ emitLine("if (outAlpha < " + HW::T_ALPHA_THRESHOLD + ")", stage, false);
+ emitScopeBegin(stage);
+ emitLine("discard", stage);
+ emitScopeEnd(stage);
+ }
+ else
+ {
+ vec4Value = "vec4f(" + outColor + ", 1.0)";
+ }
+ }
+ else if (outType.isFloat4())
+ {
+ vec4Value = outValue;
+ }
+ else if (outType.isFloat3())
+ {
+ if (context.getOptions().hwSrgbEncodeOutput)
+ outValue = "mx_srgb_encode(" + outValue + ")";
+ vec4Value = "vec4f(" + outValue + ", 1.0)";
+ }
+ else
+ {
+ vec4Value = "vec4f(vec3f(" + outValue + "), 1.0)";
+ }
+ emitLine("return " + vec4Value, stage);
+ emitScopeEnd(stage);
+
+ finalizeStageSource(stage);
+}
+
+void WgslShaderGenerator::emitDirectives(GenContext&, ShaderStage&) const
+{
+ // WGSL has no #version or preprocessor directives.
+}
+
+void WgslShaderGenerator::emitWgslSurfaceTypes(GenContext&, ShaderStage& stage) const
+{
+ stage.addString(R"WGSL(// MaterialX closure result types (multi-line so the
+// overload/broadcast type-inference pass can read their members).
+struct BSDF {
+ response: vec3f,
+ throughput: vec3f,
+}
+struct VDF {
+ response: vec3f,
+ throughput: vec3f,
+}
+alias EDF = vec3;
+struct surfaceshader {
+ color: vec3f,
+ transparency: vec3f,
+}
+struct volumeshader {
+ color: vec3f,
+ transparency: vec3f,
+}
+struct displacementshader {
+ offset: vec3f,
+ scale: f32,
+}
+alias material = surfaceshader;
+struct lightshader {
+ direction: vec3f,
+ intensity: vec3f,
+}
+)WGSL");
+ emitLineBreak(stage);
+}
+
+void WgslShaderGenerator::emitPixelUniforms(GenContext& context, ShaderStage& stage, bool lighting) const
+{
+ HwResourceBindingContextPtr resourceBindingCtx = getResourceBindingContext(context);
+ if (!resourceBindingCtx)
+ return;
+
+ auto wgslCtx = std::dynamic_pointer_cast(resourceBindingCtx);
+ wgslCtx->setBindingLocation(0);
+
+ for (const auto& it : stage.getUniformBlocks())
+ {
+ const VariableBlock& uniforms = *it.second;
+ if (uniforms.empty() || uniforms.getName() == HW::LIGHT_DATA)
+ continue;
+
+ emitComment("Uniform block: " + uniforms.getName(), stage);
+ resourceBindingCtx->emitResourceBindings(context, uniforms, stage);
+ emitLineBreak(stage);
+ }
+
+ if (lighting && context.getOptions().hwMaxActiveLightSources > 0)
+ {
+ const VariableBlock& lightData = stage.getUniformBlock(HW::LIGHT_DATA);
+ const string structArraySuffix = "[" + HW::LIGHT_DATA_MAX_LIGHT_SOURCES + "]";
+ resourceBindingCtx->emitStructuredResourceBindings(context, lightData, stage, lightData.getInstance(), structArraySuffix);
+ }
+}
+
+void WgslShaderGenerator::emitLightData(GenContext&, ShaderStage&) const
+{
+ // emitPixelUniforms() already emits the LightData struct plus u_lightData array.
+ // The GLSL base class would emit a second copy here.
+}
+
+void WgslShaderGenerator::emitInputs(GenContext& /*context*/, ShaderStage& stage) const
+{
+ DEFINE_SHADER_STAGE(stage, Stage::VERTEX)
+ {
+ const VariableBlock& vertexInputs = stage.getInputBlock(HW::VERTEX_INPUTS);
+ if (!vertexInputs.empty())
+ {
+ emitComment("Inputs block: " + vertexInputs.getName(), stage);
+ emitLine("struct VertexInput", stage, false);
+ emitScopeBegin(stage);
+ for (size_t i = 0; i < vertexInputs.size(); ++i)
+ {
+ const string name = replaceSubstrings(vertexInputs[i]->getVariable(), getTokenSubstitutions());
+ emitLine("@location(" + std::to_string(i) + ") " + name + ": " +
+ getWgslTypeName(*_syntax, vertexInputs[i]->getType()),
+ stage);
+ }
+ emitScopeEnd(stage, false);
+ emitLineBreak(stage);
+ }
+ }
+}
+
+void WgslShaderGenerator::emitVertexDataStruct(const VariableBlock& vertexData, ShaderStage& stage) const
+{
+ if (vertexData.empty())
+ return;
+ emitLine("struct " + vertexData.getName(), stage, false);
+ emitScopeBegin(stage);
+ for (size_t i = 0; i < vertexData.size(); ++i)
+ {
+ const string name = replaceSubstrings(vertexData[i]->getVariable(), getTokenSubstitutions());
+ // WGSL struct members are comma-separated (not `;`-terminated like GLSL).
+ emitLine(name + ": " + getWgslTypeName(*_syntax, vertexData[i]->getType()) + ",", stage, false);
+ }
+ emitScopeEnd(stage, false);
+ emitLineBreak(stage);
+}
+
+void WgslShaderGenerator::emitOutputs(GenContext& /*context*/, ShaderStage& stage) const
+{
+ DEFINE_SHADER_STAGE(stage, Stage::VERTEX)
+ {
+ const VariableBlock& vertexData = stage.getOutputBlock(HW::VERTEX_DATA);
+ if (!vertexData.empty())
+ {
+ emitVertexDataStruct(vertexData, stage);
+ emitLine("struct VertexOutput", stage, false);
+ emitScopeBegin(stage);
+ emitLine("@builtin(position) position: vec4f", stage);
+ emitLine("@location(" + std::to_string(vertexDataLocation) + ") " + vertexData.getInstance() + ": " +
+ vertexData.getName(),
+ stage);
+ emitScopeEnd(stage, false);
+ emitLineBreak(stage);
+ }
+ }
+}
+
+void WgslShaderGenerator::finalizeStageSource(ShaderStage& stage) const
+{
+ string src = stage.getSourceCode();
+ // HwNumLightsNode / HwLightSamplerNode emit GLSL function definitions that per-line
+ // rewriteAll() does not convert. Rewrite only those stubs (not the whole stage).
+ src = GlslToWgsl::rewriteResidualGlslFunctions(src);
+ src = GlslToWgsl::dedupDefinitions(src);
+ src = GlslToWgsl::derefPointerParams(src);
+ src = GlslToWgsl::resolveOverloads(src);
+ src = GlslToWgsl::coerceBoolCallSites(src);
+ src = GlslToWgsl::repairEmptyElseCommentBlocks(src);
+ src = GlslToWgsl::splitChainedAssignments(src);
+ stage.setSourceCode(src);
+
+ for (const string& issue : GlslToWgsl::findResidualGlsl(src))
+ {
+ std::cerr << "Warning: WgslShaderGenerator: " << issue << " in generated shader." << std::endl;
+ }
+}
+
+string WgslShaderGenerator::expandAndRewriteGlsl(const string& source, const FilePath& sourceFilename,
+ GenContext& context, GlslToWgsl::LineRewriter& rewriter) const
+{
+ // Apply token substitutions ($fileTransformUv etc.) up front so include paths
+ // and sampler/texture rewrites see resolved text. Apply longest keys first: some
+ // tokens are prefixes of others (`$envRadiance` of `$envRadianceSamples`), and the
+ // stock GLSL substitution values mask this by accident (`u_envRadiance`+`Samples` ==
+ // `u_envRadianceSamples`) — but the split sampler values used here do not, so an
+ // unordered pass would corrupt the longer token.
+ std::vector> subs(getTokenSubstitutions().begin(),
+ getTokenSubstitutions().end());
+ std::sort(subs.begin(), subs.end(),
+ [](const std::pair& a, const std::pair& b)
+ {
+ return a.first.length() > b.first.length();
+ });
+ string substituted = source;
+ for (const auto& pair : subs)
+ {
+ if (pair.first.empty())
+ continue;
+ size_t pos = 0;
+ while ((pos = substituted.find(pair.first, pos)) != string::npos)
+ {
+ substituted.replace(pos, pair.first.length(), pair.second);
+ pos += pair.second.length();
+ }
+ }
+
+ std::istringstream stream(substituted);
+ string line;
+ string out;
+ out.reserve(substituted.size() + substituted.size() / 8);
+
+ while (std::getline(stream, line))
+ {
+ const bool hadCr = !line.empty() && line.back() == '\r';
+ if (hadCr)
+ line.pop_back();
+
+ const size_t firstNonSpace = line.find_first_not_of(" \t");
+ const bool isInclude = (firstNonSpace != string::npos &&
+ line.compare(firstNonSpace, 8, "#include") == 0);
+ if (isInclude)
+ {
+ const size_t q1 = line.find('"', firstNonSpace + 8);
+ const size_t q2 = (q1 == string::npos) ? string::npos : line.find('"', q1 + 1);
+ if (q1 == string::npos || q2 == string::npos || q2 <= q1 + 1)
+ continue; // malformed include: drop it
+ const string includePath = line.substr(q1 + 1, q2 - q1 - 1);
+ // Expand each header only once per generation (avoids duplicate WGSL defs).
+ auto seen = context.getUserData(WGSL_INCLUDES);
+ const string baseName = FilePath(includePath).getBaseName();
+ if (seen && seen->basenames.count(baseName))
+ continue;
+ if (seen)
+ seen->basenames.insert(baseName);
+ const FilePath resolved = context.resolveSourceFile(includePath, sourceFilename.getParentPath());
+ const string content = readFile(resolved);
+ if (!content.empty())
+ out += expandAndRewriteGlsl(content, resolved, context, rewriter);
+ continue;
+ }
+
+ out += rewriter.rewrite(line);
+ if (hadCr)
+ out += '\r';
+ out += '\n';
+ }
+ return out;
+}
+
+MATERIALX_NAMESPACE_END
diff --git a/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h
new file mode 100644
index 0000000000..fa06a9e7c1
--- /dev/null
+++ b/source/MaterialXGenGlsl/wgsl/WgslShaderGenerator.h
@@ -0,0 +1,90 @@
+//
+// Copyright Contributors to the MaterialX Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#ifndef MATERIALX_WGSLSHADERGENERATOR_H
+#define MATERIALX_WGSLSHADERGENERATOR_H
+
+/// @file
+/// WGSL shader generator
+
+#include
+#include
+#include
+
+MATERIALX_NAMESPACE_BEGIN
+
+// The GLSL->WGSL text utilities are internal to MaterialXGenGlsl (see wgsl/converter/GlslToWgsl.h);
+// only the incomplete LineRewriter type is referenced here, so a forward declaration keeps
+// that internal header out of the installed public API.
+namespace GlslToWgsl
+{
+class LineRewriter;
+}
+
+using WgslShaderGeneratorPtr = shared_ptr;
+
+/// A WGSL (WebGPU Shading Language) shader generator.
+/// Reuses the genglsl emit pipeline (via VkShaderGenerator) and converts emitted GLSL to WGSL
+/// at emit time via the GlslToWgsl utilities.
+class MX_GENGLSL_API WgslShaderGenerator : public VkShaderGenerator
+{
+ public:
+ /// Constructor.
+ WgslShaderGenerator(TypeSystemPtr typeSystem);
+
+ /// Creator function.
+ /// If a TypeSystem is not provided it will be created internally.
+ /// Optionally pass in an externally created TypeSystem here,
+ /// if you want to keep type descriptions alive after the lifetime
+ /// of the shader generator.
+ static ShaderGeneratorPtr create(TypeSystemPtr typeSystem = nullptr)
+ {
+ return std::make_shared(typeSystem ? typeSystem : TypeSystem::create());
+ }
+
+ /// Return a unique identifier for the target this generator is for
+ const string& getTarget() const override { return TARGET; }
+
+ const string& getLightDataTypevarString() const override { return LIGHTDATA_TYPEVAR_STRING; }
+
+ ShaderNodeImplPtr getImplementation(const NodeDef& nodedef, GenContext& context) const override;
+
+ /// Unique identifier for this generator target
+ static const string TARGET;
+
+ /// Emit a shader variable.
+ void emitVariableDeclaration(const ShaderPort* variable, const string& qualifier, GenContext& context, ShaderStage& stage, bool assignValue = true) const override;
+ void emitFunctionDefinitionParameter(const ShaderPort* shaderPort, bool isOutput, GenContext& context, ShaderStage& stage) const override;
+ void emitInput(const ShaderInput* input, GenContext& context, ShaderStage& stage) const override;
+ void emitOutput(const ShaderOutput* output, bool includeType, bool assignValue, GenContext& context, ShaderStage& stage) const override;
+ void emitString(const string& str, ShaderStage& stage) const override;
+ void emitLine(const string& str, ShaderStage& stage, bool semicolon = true) const override;
+ void emitLineEnd(ShaderStage& stage, bool semicolon = true) const override;
+ void emitBlock(const string& str, const FilePath& sourceFilename, GenContext& context, ShaderStage& stage) const override;
+ void emitLibraryInclude(const FilePath& filename, GenContext& context, ShaderStage& stage) const override;
+ void emitTypeDefinitions(GenContext& context, ShaderStage& stage) const override;
+
+ protected:
+ void emitVertexStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const override;
+ void emitPixelStage(const ShaderGraph& graph, GenContext& context, ShaderStage& stage) const override;
+
+ void emitDirectives(GenContext& context, ShaderStage& stage) const override;
+ void emitWgslSurfaceTypes(GenContext& context, ShaderStage& stage) const;
+ void emitPixelUniforms(GenContext& context, ShaderStage& stage, bool lighting) const;
+ void emitLightData(GenContext& context, ShaderStage& stage) const override;
+ void emitInputs(GenContext& context, ShaderStage& stage) const override;
+ void emitVertexDataStruct(const VariableBlock& vertexData, ShaderStage& stage) const;
+ void emitOutputs(GenContext& context, ShaderStage& stage) const override;
+
+ void finalizeStageSource(ShaderStage& stage) const;
+ string expandAndRewriteGlsl(const string& source, const FilePath& sourceFilename,
+ GenContext& context, GlslToWgsl::LineRewriter& rewriter) const;
+
+ static const string LIGHTDATA_TYPEVAR_STRING;
+};
+
+MATERIALX_NAMESPACE_END
+
+#endif
diff --git a/source/MaterialXGenGlsl/WgslSyntax.cpp b/source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp
similarity index 98%
rename from source/MaterialXGenGlsl/WgslSyntax.cpp
rename to source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp
index d5d58dc014..63065deeea 100644
--- a/source/MaterialXGenGlsl/WgslSyntax.cpp
+++ b/source/MaterialXGenGlsl/wgsl/WgslSyntax.cpp
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
-#include
+#include
MATERIALX_NAMESPACE_BEGIN
diff --git a/source/MaterialXGenGlsl/WgslSyntax.h b/source/MaterialXGenGlsl/wgsl/WgslSyntax.h
similarity index 92%
rename from source/MaterialXGenGlsl/WgslSyntax.h
rename to source/MaterialXGenGlsl/wgsl/WgslSyntax.h
index 0e16f221bd..f53e0f9503 100644
--- a/source/MaterialXGenGlsl/WgslSyntax.h
+++ b/source/MaterialXGenGlsl/wgsl/WgslSyntax.h
@@ -9,7 +9,7 @@
/// @file
/// Vulkan GLSL syntax class for WGSL
-#include
+#include
MATERIALX_NAMESPACE_BEGIN
diff --git a/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp
new file mode 100644
index 0000000000..c1c981174b
--- /dev/null
+++ b/source/MaterialXGenGlsl/wgsl/converter/GlslToWgsl.cpp
@@ -0,0 +1,3829 @@
+//
+// Copyright Contributors to the MaterialX Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include
+
+#include
+#include
+#include
+#include
+#include