This commit is contained in:
2026-01-06 13:25:49 +00:00
parent 5d495d731b
commit 4e15e08b7f
1395 changed files with 295666 additions and 323 deletions

View File

@@ -0,0 +1,375 @@
import * as THREE from 'three';
import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
const debugFileHandling = false;
let loadingCallback = null;
let sceneLoadingCallback = null;
export function setLoadingCallback(cb)
{
loadingCallback = cb;
}
export function setSceneLoadingCallback(cb)
{
sceneLoadingCallback = cb;
}
export function dropHandler(ev)
{
if (debugFileHandling) console.log('File(s) dropped', ev.dataTransfer.items, ev.dataTransfer.files);
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
if (ev.dataTransfer.items)
{
const allEntries = [];
let haveGetAsEntry = false;
if (ev.dataTransfer.items.length > 0)
haveGetAsEntry =
("getAsEntry" in ev.dataTransfer.items[0]) ||
("webkitGetAsEntry" in ev.dataTransfer.items[0]);
// Useful for debugging file handling on platforms that don't support newer file system APIs
// haveGetAsEntry = false;
if (haveGetAsEntry)
{
for (var i = 0; i < ev.dataTransfer.items.length; i++)
{
let item = ev.dataTransfer.items[i];
let entry = ("getAsEntry" in item) ? item.getAsEntry() : item.webkitGetAsEntry();
allEntries.push(entry);
}
handleFilesystemEntries(allEntries);
return;
}
for (var i = 0; i < ev.dataTransfer.items.length; i++)
{
let item = ev.dataTransfer.items[i];
// API when there's no "getAsEntry" support
console.log(item.kind, item);
if (item.kind === 'file')
{
var file = item.getAsFile();
testAndLoadFile(file);
}
// could also be a directory
else if (item.kind === 'directory')
{
var dirReader = item.createReader();
dirReader.readEntries(function (entries)
{
for (var i = 0; i < entries.length; i++)
{
console.log(entries[i].name);
var entry = entries[i];
if (entry.isFile)
{
entry.file(function (file)
{
testAndLoadFile(file);
});
}
}
});
}
}
} else
{
for (var i = 0; i < ev.dataTransfer.files.length; i++)
{
let file = ev.dataTransfer.files[i];
testAndLoadFile(file);
}
}
}
export function dragOverHandler(ev)
{
ev.preventDefault();
}
async function getBufferFromFile(fileEntry)
{
if (fileEntry instanceof ArrayBuffer) return fileEntry;
if (fileEntry instanceof String) return fileEntry;
const name = fileEntry.fullPath || fileEntry.name;
const ext = name.split('.').pop();
const readAsText = ext === 'mtlx';
if (debugFileHandling) console.log("reading ", fileEntry, "as text?", readAsText);
if (debugFileHandling) console.log("getBufferFromFile", fileEntry);
const buffer = await new Promise((resolve, reject) =>
{
function readFile(file)
{
var reader = new FileReader();
reader.onloadend = function (e)
{
if (debugFileHandling) console.log("loaded", "should be text?", readAsText, this.result);
resolve(this.result);
};
if (readAsText)
reader.readAsText(file);
else
reader.readAsArrayBuffer(file);
}
if ("file" in fileEntry)
{
fileEntry.file(function (file)
{
readFile(file);
}, (e) =>
{
console.error("Error reading file ", e);
});
}
else
{
readFile(fileEntry);
}
});
return buffer;
}
async function handleFilesystemEntries(entries)
{
const allFiles = [];
const fileIgnoreList = [
'.gitignore',
'README.md',
'.DS_Store',
]
const dirIgnoreList = [
'.git',
'node_modules',
]
let isGLB = false;
let haveMtlx = false;
for (let entry of entries)
{
if (debugFileHandling) console.log("file entry", entry)
if (entry.isFile)
{
if (debugFileHandling)
console.log("single file", entry);
if (fileIgnoreList.includes(entry.name))
{
continue;
}
allFiles.push(entry);
if (entry.name.endsWith('glb'))
{
isGLB = true;
break;
}
}
else if (entry.isDirectory)
{
if (dirIgnoreList.includes(entry.name))
{
continue;
}
const files = await readDirectory(entry);
if (debugFileHandling) console.log("all files", files);
for (const file of files)
{
if (fileIgnoreList.includes(file.name))
{
continue;
}
allFiles.push(file);
}
}
}
const imageLoader = new THREE.ImageLoader();
// unpack zip files first
for (const fileEntry of allFiles)
{
// special case: zip archives
if (fileEntry.fullPath.toLowerCase().endsWith('.zip'))
{
await new Promise(async (resolve, reject) =>
{
const arrayBuffer = await getBufferFromFile(fileEntry);
// use fflate to unpack them and add the files to the cache
fflate.unzip(new Uint8Array(arrayBuffer), (error, unzipped) =>
{
// push these files into allFiles
for (const [filePath, buffer] of Object.entries(unzipped))
{
// mock FileEntry for easier usage downstream
const blob = new Blob([buffer]);
const newFileEntry = {
fullPath: "/" + filePath,
name: filePath.split('/').pop(),
file: (callback) =>
{
callback(blob);
},
isFile: true,
};
allFiles.push(newFileEntry);
}
resolve();
});
});
}
}
// sort so mtlx files come first
allFiles.sort((a, b) =>
{
if (a.name.endsWith('.mtlx') && !b.name.endsWith('.mtlx'))
{
return -1;
}
if (!a.name.endsWith('.mtlx') && b.name.endsWith('.mtlx'))
{
return 1;
}
return 0;
});
if (isGLB)
{
console.log("Load GLB file", allFiles[0]);
const rootFile = allFiles[0];
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
sceneLoadingCallback(rootFile);
return;
}
if (!allFiles[0].name.endsWith('mtlx'))
{
console.log("No MaterialX files dropped. Skipping content.");
return;
}
if (debugFileHandling)
{
console.log("- All files", allFiles);
}
// put all files in three' Cache
for (const fileEntry of allFiles)
{
const allowedFileTypes = [
'png', 'jpg', 'jpeg'
];
const ext = fileEntry.fullPath.split('.').pop();
if (!allowedFileTypes.includes(ext))
{
// console.log("skipping file", fileEntry.fullPath);
continue;
}
const buffer = await getBufferFromFile(fileEntry);
const img = await imageLoader.loadAsync(URL.createObjectURL(new Blob([buffer])));
if (debugFileHandling) console.log("caching file", fileEntry.fullPath, img);
THREE.Cache.add(fileEntry.fullPath, img);
}
// TODO we could also allow dropping of multiple MaterialX files (or folders with them inside)
// and seed the dropdown from that.
// At that point, actually reading files and textures into memory should be deferred until they are actually used.
if (allFiles.length > 0)
{
const rootFile = allFiles[0];
THREE.Cache.add(rootFile.fullPath, await getBufferFromFile(rootFile));
if (debugFileHandling) console.log("CACHE", THREE.Cache.files);
loadingCallback(rootFile);
}
else
{
console.log('No files to add cache.')
}
}
async function readDirectory(directory)
{
let entries = [];
let getEntries = async (directory) =>
{
let dirReader = directory.createReader();
await new Promise((resolve, reject) =>
{
dirReader.readEntries(
async (results) =>
{
if (results.length)
{
// entries = entries.concat(results);
for (let entry of results)
{
if (entry.isDirectory)
{
await getEntries(entry);
}
else
{
entries.push(entry);
}
}
}
resolve();
},
(error) =>
{
/* handle error — error is a FileError object */
},
)
}
)
};
await getEntries(directory);
return entries;
}
async function testAndLoadFile(file)
{
let ext = file.name.split('.').pop();
if (debugFileHandling) console.log(file.name + ", " + file.size + ", " + ext);
const arrayBuffer = await getBufferFromFile(file);
console.log(arrayBuffer)
// mock a fileEntry and pass through the same loading logic
const newFileEntry = {
fullPath: "/" + file.name,
name: file.name.split('/').pop(),
isFile: true,
file: (callback) =>
{
callback(file);
}
};
handleFilesystemEntries([newFileEntry]);
}

View File

@@ -0,0 +1,352 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//
import * as THREE from 'three';
const IMAGE_PROPERTY_SEPARATOR = "_";
const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
const VADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "vaddressmode";
const FILTER_TYPE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "filtertype";
const IMAGE_PATH_SEPARATOR = "/";
/**
* Initialized the environment texture as MaterialX expects it
* @param {THREE.Texture} texture
* @param {Object} capabilities
* @returns {THREE.Texture}
*/
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();
newTexture.minFilter = THREE.LinearMipmapLinearFilter;
newTexture.magFilter = THREE.LinearFilter;
newTexture.generateMipmaps = true;
newTexture.needsUpdate = true;
return newTexture;
}
/**
* Get Three uniform from MaterialX vector
* @param {any} value
* @param {any} dimension
* @returns {THREE.Uniform}
*/
function fromVector(value, dimension)
{
let outValue;
if (value)
{
outValue = value.data();
}
else
{
outValue = [];
for (let i = 0; i < dimension; ++i)
outValue.push(0.0);
}
return outValue;
}
/**
* Get Three uniform from MaterialX matrix
* @param {mx.matrix} matrix
* @param {mx.matrix.size} dimension
*/
function fromMatrix(matrix, dimension)
{
let vec = [];
if (matrix)
{
for (let i = 0; i < matrix.numRows(); ++i)
{
for (let k = 0; k < matrix.numColumns(); ++k)
{
vec.push(matrix.getItem(i, k));
}
}
} else
{
for (let i = 0; i < dimension; ++i)
vec.push(0.0);
}
return vec;
}
/**
* Get Three uniform from MaterialX value
* @param {mx.Uniform.type} type
* @param {mx.Uniform.value} value
* @param {mx.Uniform.name} name
* @param {mx.Uniforms} uniforms
* @param {THREE.textureLoader} textureLoader
*/
function toThreeUniform(type, value, name, uniforms, textureLoader, searchPath, flipY)
{
let outValue = null;
switch (type)
{
case 'float':
case 'integer':
case 'boolean':
outValue = value;
break;
case 'vector2':
outValue = fromVector(value, 2);
break;
case 'vector3':
case 'color3':
outValue = fromVector(value, 3);
break;
case 'vector4':
case 'color4':
outValue = fromVector(value, 4);
break;
case 'matrix33':
outValue = fromMatrix(value, 9);
break;
case 'matrix44':
outValue = fromMatrix(value, 16);
break;
case 'filename':
if (value)
{
// Cache / reuse texture to avoid reload overhead.
// Note: that data blobs and embedded data textures are not cached as they are transient data.
let checkCache = true;
let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
if (value.startsWith('blob:'))
{
texturePath = value;
console.log('Load blob URL:', texturePath);
checkCache = false;
}
else if (value.startsWith('http'))
{
texturePath = value;
console.log('Load HTTP URL:', texturePath);
}
else if (value.startsWith('data:'))
{
texturePath = value;
checkCache = false;
console.log('Load data URL:', texturePath);
}
const cachedTexture = checkCache && THREE.Cache.get(texturePath);
if (cachedTexture)
{
// Get texture from cache
outValue = cachedTexture;
console.log('Use cached texture: ', texturePath, outValue);
}
else
{
outValue = textureLoader.load(
texturePath,
function (texture) {
console.log('Load new texture: ' + texturePath, texture);
outValue = texture;
// Add texture to ThreeJS cache
if (checkCache)
THREE.Cache.add(texturePath, texture);
},
undefined,
function (error) {
console.error('Error loading texture: ', error);
});
// Set address & filtering mode
if (outValue)
setTextureParameters(outValue, name, uniforms, flipY);
}
}
break;
case 'samplerCube':
case 'string':
break;
default:
console.log('Value type not supported: ' + type);
outValue = null;
}
return outValue;
}
/**
* Get Three wrapping mode
* @param {mx.TextureFilter.wrap} mode
* @returns {THREE.Wrapping}
*/
function getWrapping(mode)
{
let wrap;
switch (mode)
{
case 1:
wrap = THREE.ClampToEdgeWrapping;
break;
case 2:
wrap = THREE.RepeatWrapping;
break;
case 3:
wrap = THREE.MirroredRepeatWrapping;
break;
default:
wrap = THREE.RepeatWrapping;
break;
}
return wrap;
}
/**
* Get Three minification filter
* @param {mx.TextureFilter.minFilter} type
* @param {mx.TextureFilter.generateMipmaps} generateMipmaps
*/
function getMinFilter(type, generateMipmaps)
{
const filterType = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
if (type === 0)
{
filterType = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
}
return filterType;
}
/**
* Set Three texture parameters
* @param {THREE.Texture} texture
* @param {mx.Uniform.name} name
* @param {mx.Uniforms} uniforms
* @param {mx.TextureFilter.generateMipmaps} generateMipmaps
*/
function setTextureParameters(texture, name, uniforms, flipY = true, generateMipmaps = true)
{
const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
const base = name.substring(0, idx) || name;
texture.generateMipmaps = generateMipmaps;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.flipY = flipY;
if (uniforms.find(base + UADDRESS_MODE_SUFFIX))
{
const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
texture.wrapS = getWrapping(uaddressmode);
}
if (uniforms.find(base + VADDRESS_MODE_SUFFIX))
{
const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
texture.wrapT = getWrapping(vaddressmode);
}
const filterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
texture.minFilter = getMinFilter(filterType, generateMipmaps);
}
/**
* Return the global light rotation matrix
*/
export function getLightRotation()
{
return new THREE.Matrix4().makeRotationY(Math.PI / 2);
}
/**
* Returns all lights nodes in a MaterialX document
* @param {mx.Document} doc
* @returns {Array.<mx.Node>}
*/
export function findLights(doc)
{
let lights = [];
for (let node of doc.getNodes())
{
if (node.getType() === "lightshader")
lights.push(node);
}
return lights;
}
/**
* Register lights in shader generation context
* @param {Object} mx MaterialX Module
* @param {Array.<mx.Node>} lights Light nodes
* @param {mx.GenContext} genContext Shader generation context
* @returns {Array.<mx.Node>}
*/
export function registerLights(mx, lights, genContext)
{
mx.HwShaderGenerator.unbindLightShaders(genContext);
const lightTypesBound = {};
const lightData = [];
let lightId = 1;
for (let light of lights)
{
let nodeDef = light.getNodeDef();
let nodeName = nodeDef.getName();
if (!lightTypesBound[nodeName])
{
lightTypesBound[nodeName] = lightId;
mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
}
const lightDirection = light.getValueElement("direction").getValue().getData().data();
const lightColor = light.getValueElement("color").getValue().getData().data();
const lightIntensity = light.getValueElement("intensity").getValue().getData();
let rotatedLightDirection = new THREE.Vector3(...lightDirection)
rotatedLightDirection.transformDirection(getLightRotation())
lightData.push({
type: lightTypesBound[nodeName],
direction: rotatedLightDirection,
color: new THREE.Vector3(...lightColor),
intensity: lightIntensity
});
}
// Make sure max light count is large enough
genContext.getOptions().hwMaxActiveLightSources = Math.max(genContext.getOptions().hwMaxActiveLightSources, lights.length);
return lightData;
}
/**
* Get uniform values for a shader
* @param {mx.shaderStage} shaderStage
* @param {THREE.TextureLoader} textureLoader
*/
export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
{
let threeUniforms = {};
const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
uniformBlocks.forEach(uniforms =>
{
if (!uniforms.empty())
{
for (let i = 0; i < uniforms.size(); ++i)
{
const variable = uniforms.get(i);
const value = variable.getValue()?.getData();
const name = variable.getVariable();
threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
textureLoader, searchPath, flipY));
}
}
});
return threeUniforms;
}

View File

@@ -0,0 +1,198 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//
import * as THREE from 'three';
import { Viewer } from './viewer.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { dropHandler, dragOverHandler, setLoadingCallback, setSceneLoadingCallback } from './dropHandling.js';
let renderer, orbitControls;
// Turntable option. For now the step size is fixed.
let turntableEnabled = false;
let turntableSteps = 360;
let turntableStep = 0;
let captureRequested = false;
// Get URL options. Fallback to defaults if not specified.
let materialFilename = new URLSearchParams(document.location.search).get("file");
if (!materialFilename)
{
materialFilename = 'Materials/Examples/StandardSurface/standard_surface_default.mtlx';
}
let viewer = Viewer.create();
init();
viewer.getEditor().updateProperties(0.9);
// Capture the current frame and save an image file.
function captureFrame()
{
let canvas = document.getElementById('webglcanvas');
var url = canvas.toDataURL();
var link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('target', '_blank');
link.setAttribute('download', 'screenshot.png');
link.click();
}
function init()
{
let canvas = document.getElementById('webglcanvas');
// Handle material selection changes
let materialsSelect = document.getElementById('materials');
materialsSelect.value = materialFilename;
materialsSelect.addEventListener('change', (e) =>
{
materialFilename = e.target.value;
viewer.getEditor().initialize();
viewer.getMaterial().loadMaterials(viewer, materialFilename);
viewer.getEditor().updateProperties(0.9);
});
// Handle geometry selection changes
const scene = viewer.getScene();
let geometrySelect = document.getElementById('geometry');
geometrySelect.value = scene.getGeometryURL();
geometrySelect.addEventListener('change', (e) =>
{
console.log('Change geometry to:', e.target.value);
scene.setGeometryURL(e.target.value);
scene.loadGeometry(viewer, orbitControls);
});
// Set up scene
scene.initialize();
// Set up renderer
renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.debug.checkShaderErrors = false;
window.addEventListener('resize', onWindowResize);
// Set up controls
orbitControls = new OrbitControls(scene.getCamera(), renderer.domElement);
// Add hotkey 'f' to capture the current frame and save an image file.
// See check inside the render loop when a capture can be performed.
document.addEventListener('keydown', (event) =>
{
if (event.key === 'f')
{
captureRequested = true;
}
});
// Initialize editor
viewer.getEditor().initialize();
const hdrLoader = viewer.getHdrLoader();
const fileLoader = viewer.getFileLoader();
Promise.all([
new Promise(resolve => hdrLoader.load('Lights/san_giuseppe_bridge_split.hdr', resolve)),
new Promise(resolve => hdrLoader.load('Lights/irradiance/san_giuseppe_bridge_split.hdr', resolve)),
new Promise(resolve => fileLoader.load('Lights/san_giuseppe_bridge_split.mtlx', resolve)),
new Promise(function (resolve)
{
MaterialX().then((module) =>
{
resolve(module);
});
})
]).then(async ([radianceTexture, irradianceTexture, lightRigXml, mxIn]) =>
{
// Initialize viewer + lighting
await viewer.initialize(mxIn, renderer, radianceTexture, irradianceTexture, lightRigXml);
// Load geometry
let scene = viewer.getScene();
scene.loadGeometry(viewer, orbitControls);
// Load materials
viewer.getMaterial().loadMaterials(viewer, materialFilename);
// Update assignments
viewer.getMaterial().updateMaterialAssignments(viewer);
canvas.addEventListener("keydown", handleKeyEvents, true);
}).then(() =>
{
animate();
}).catch(err =>
{
console.error(Number.isInteger(err) ? this.getMx().getExceptionMessage(err) : err);
})
// allow dropping files and directories
document.addEventListener('drop', dropHandler, false);
document.addEventListener('dragover', dragOverHandler, false);
setLoadingCallback(file =>
{
materialFilename = file.fullPath || file.name;
viewer.getEditor().initialize();
viewer.getMaterial().loadMaterials(viewer, materialFilename);
viewer.getEditor().updateProperties(0.9);
});
setSceneLoadingCallback(file =>
{
let glbFileName = file.fullPath || file.name;
console.log('Drop geometry to:', glbFileName);
scene.setGeometryURL(glbFileName);
scene.loadGeometry(viewer, orbitControls);
});
// enable three.js Cache so that dropped files can reference each other
THREE.Cache.enabled = true;
}
function onWindowResize()
{
viewer.getScene().updateCamera();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate()
{
requestAnimationFrame(animate);
const scene = viewer.getScene();
if (turntableEnabled)
{
turntableStep = (turntableStep + 1) % 360;
var turntableAngle = turntableStep * (360.0 / turntableSteps) / 180.0 * Math.PI;
scene._scene.rotation.y = turntableAngle;
}
scene.updateTimeUniforms();
renderer.render(scene.getScene(), scene.getCamera());
if (captureRequested)
{
captureFrame();
captureRequested = false;
}
}
function handleKeyEvents(event)
{
const V_KEY = 86;
const P_KEY = 80;
if (event.keyCode == V_KEY)
{
viewer.getScene().toggleBackgroundTexture();
}
else if (event.keyCode == P_KEY)
{
turntableEnabled = !turntableEnabled;
}
}

File diff suppressed because it is too large Load Diff