I'm trying to assign a new material to an object, but when I assign a new (color) map, the object renders as white, and the AO and shadows no longer show up. It's as if the emissive attribute is 100%. I can change the color attribute (e.g. 'red' or 'blue'), ao, normal, etc. without issues. The glb loaded in already has a working material with a color map and ao, but I want to be able to replace it.
I'm using 8th Wall with A-Frame, but I've registered the following as a custom Three.js component.
const customMat = {
schema: {}, // will pass textures via aframe later
init() {
this.el.addEventListener('model-loaded', (e) => {
const material = new THREE.MeshStandardMaterial()
const texLoader = new THREE.TextureLoader()
texLoader.crossOrigin = ''
const mapColor = texLoader.load('assets/cover_color.jpg')
const mapAO = texLoader.load('assets/cover_ao.jpg')
material.map = mapColor // makes everything 100% white likes it's emissive
// material.color = new THREE.Color('red') // works fine no problem
material.aoMap = mapAO
material.aoMapIntensity = 1
e.detail.model.traverse((mesh) => {
if (mesh.isMesh) {
mesh.material = material
mesh.material.needsUpdate = true // not sure if needed
}
})
})
},
}
export {customMat}
Any suggestions would be much appreciated. I've tried this with primitive geometry too, but the same issue occurs. I don't seem to be able to modify the existing material's attributes either, so maybe my approach is fundamentally wrong.
I've imported a GLTF file with two different meshes. My goal is to give each mesh a material with a unique custom fragment shader using onBeforeCompile. Each mesh has the same type of material (MeshNormalMaterial).
When I try to apply one fragment shader to one material and the other fragment shader to the other material, both materials wind up with the same fragment shader. The fragment shader each material has depends on which material I setup first.
Here's a few pictures showing what I'm talking about:
Below is all the relevant code.
Main code: This is the general structure of my code. I've enclosed the important part between "PRIMARY AREA OF INTEREST" comments. For simplicity, I've replaced my shader code with "..." or a comment describing what it does. They do work as shown in the pictures above.
// Three.JS Canvas
const threeDisplay = document.getElementById("threeDisplay");
// Globals
var displayDimensions = getElemDimensions(threeDisplay); // Uniform
var currentTime = 0; // Uniform
var helix = null; // Mesh
var innerHelix = null; // Mesh
var horseshoe = null; // Mesh
// Set the scene and camera up
const scene = new THREE.Scene();
const camera = initCamera();
// Setup a directional light
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set(-0.2, 1, -0.6);
scene.add(light);
// Setup WebGL renderer
const renderer = initRenderer();
threeDisplay.appendChild( renderer.domElement );
// Load the gltf model
new GLTFLoader().load( "./spiral_pillar_hq_horseshoe.glb", function (object) {
const helixFragmentShaderReplacements = [
{
from: ' ... ',
to: ' // rainbow '
}
];
const horseshoeFragmentShaderReplacements = [
{
from: ' ... ',
to: ' // white '
}
];
//////////////////////////////////////
// PRIMARY AREA OF INTEREST - START //
//////////////////////////////////////
// Turn the horseshoe into a shader.
horseshoe = object.scene.children[1];
var horseshoeGeometry = horseshoe.geometry;
var horseshoeMaterial = shaderMeshMaterial(new THREE.MeshNormalMaterial(), horseshoeGeometry, horseshoeFragmentShaderReplacements);
var horseshoeMesh = new THREE.Mesh(horseshoeGeometry, horseshoeMaterial);
horseshoe = horseshoeMesh;
horseshoe.rotation.z = deg2rad(180); // Re-orient the horseshoe to the correct position and rotation.
horseshoe.position.y = 13;
scene.add(horseshoe);
// Turn the inner helix into a colorful, wiggly shader.
helix = object.scene.children[0];
var helixGeometry = helix.geometry;
var helixMaterial = shaderMeshMaterial(new THREE.MeshNormalMaterial(), helixGeometry, helixFragmentShaderReplacements);
var helixMesh = new THREE.Mesh(helixGeometry, helixMaterial);
helix = helixMesh;
scene.add(innerHelix);
animate();
////////////////////////////////////
// PRIMARY AREA OF INTEREST - END //
////////////////////////////////////
}, undefined, function (error) {
console.error(error);
});
Below are functions which are relevant.
shaderMeshMaterial: Constructs a new material based on the supplied materialType that supports editing the default shader. If it's not initProcessing, then the problem may stem from this function.
// Globals used: displayDimensions
function shaderMeshMaterial(materialType, geometry, fragmentShaderReplacements) {
var material = materialType;
material.onBeforeCompile = function ( shader ) {
// Uniforms
shader.uniforms.time = { value: 0 };
shader.uniforms.resolution = { value: new THREE.Vector2(displayDimensions.width, displayDimensions.height) };
shader.uniforms.bboxMin = { value: geometry.boundingBox.min };
shader.uniforms.bboxMax = { value: geometry.boundingBox.max };
fragmentShaderReplacements.forEach((rep) => {
shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
});
console.log(shader);
material.userData.shader = shader;
}
return material;
}
initRenderer: Sets up the renderer. Just showing you guys the renderer setup I have in case that's important.
// Globals used: displayDimensions
function initRenderer() {
var renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
precision: "mediump"
});
renderer.setClearColor( 0x000000, 0);
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( displayDimensions.width, displayDimensions.height );
renderer.shadowMap.enabled = true;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
return renderer;
}
animate: Handles the animation frames.
// Globals used: renderer, currentTime, postprocessing
function animate (timestamp = 0) {
requestAnimationFrame(animate);
resizeRendererToDisplaySize(renderer);
currentTime = timestamp/1000; // Current time in seconds.
scene.traverse( function ( child ) {
if ( child.isMesh ) {
const shader = child.material.userData.shader;
if ( shader ) {
shader.uniforms.time.value = currentTime;
}
}
} );
renderer.render( scene, camera );
postprocessing.composer.render( 0.1 );
};
One last thing to note is that when I inspected the console log of shader from the shaderMeshMaterial function, I can see that the fragment shaders are indeed different as they should be for each material. Also not sure why there are 4 console logs when there should only be 2.
Sorry for all the code, but I did condense it to where all irrelevant code was stripped out. I'm fairly new to Three.JS, so any possible explanations as to why this is happening are much appreciated!
EDIT: Removed vertex shader parameter from shaderMeshMaterial function to keep this question focused on just the fragment shaders. Though this problem does apply to both the vertex and fragment shaders, I figure if you fix one then you'll fix the other.
EDIT 2: Added language identifiers to code snippets. Also I removed the postprocessing function and the problem still persists, so I know the problem isn't caused by that. I've updated the code above to reflect this change. As a happy side effect of removing the postprocessing function, the console.log of the shader variable from shaderMeshMaterial new appears twice in the log (as it should).
EDIT 3: (Implementing WestLangley's suggestion) I tweaked the shaderMeshMaterial function by adding the customProgramCacheKey function. I had to condense the four parameters of shaderMeshMaterial into one for the sake of the customProgramCacheKey function. I believe I implemented the function correctly, but I'm still getting the same result as before where both materials display the same fragment shader.
New "PRIMARY AREA OF INTEREST" code:
horseshoe = object.scene.children[1];
var horseshoeGeometry = horseshoe.geometry;
var meshData = {
materialType: new THREE.MeshNormalMaterial(),
geometry: horseshoeGeometry,
fragmentShaderReplacements: horseshoeFragmentShaderReplacements
}
var horseshoeMaterial = shaderMeshMaterial(meshData);
var horseshoeMesh = new THREE.Mesh(horseshoeGeometry, horseshoeMaterial);
horseshoe = horseshoeMesh;
horseshoe.rotation.z = deg2rad(180); // Re-orient the horseshoe to the correct position and rotation.
horseshoe.position.y = 13;
scene.add(horseshoe);
// Turn the inner helix into a colorful, wiggly shader.
helix = object.scene.children[0];
var helixGeometry = helix.geometry;
var meshData2 = {
materialType: new THREE.MeshNormalMaterial(),
geometry: helixGeometry,
fragmentShaderReplacements: helixFragmentShaderReplacements
}
var helixMaterial = shaderMeshMaterial(meshData2);
var helixMesh = new THREE.Mesh(helixGeometry, helixMaterial);
helix = helixMesh;
scene.add(innerHelix);
animate();
New shaderMeshMaterial code:
// Globals used: displayDimensions
function shaderMeshMaterial(meshData) {
var material = meshData.materialType;
material.onBeforeCompile = function ( shader ) {
// Uniforms
shader.uniforms.time = { value: 0 };
shader.uniforms.resolution = { value: new THREE.Vector2(displayDimensions.width, displayDimensions.height) };
shader.uniforms.bboxMin = { value: meshData.geometry.boundingBox.min };
shader.uniforms.bboxMax = { value: meshData.geometry.boundingBox.max };
meshData.fragmentShaderReplacements.forEach((rep) => {
shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
});
material.customProgramCacheKey = function () {
return meshData;
};
console.log(shader);
material.userData.shader = shader;
}
return material;
}
WestLangley suggestion worked for me!
material.onBeforeCompile = ...
// Make sure WebGLRenderer doesnt reuse a single program
material.customProgramCacheKey = function () {
return UNIQUE_PER_MATERIAL_ID;
};
I believe your mistake is returning meshData from customProgramCacheKey.
I think customProgramCacheKey need concrete identifier like a number or string.
It would be nice to understand what exactly happening and why do we need to specify customProgramCacheKey.
EDIT: I discover that default value for customProgramCacheKey calculated as follow in Threejs source.
customProgramCacheKey() {
return this.onBeforeCompile.toString();
}
Perhaps this is explains this default caching behavior because calling toString on function returns that function body literally as string.
For example consider function const myFunc = () => { return 1 }. Calling myFunc.toString() returns "() => { return 1 }"
So if your calling onBeforeCompile in a for loop you function body as string never change.
I'm displaying some GeoJSON as markers in Cesium; they do not have altitude, so I'm using clampToGround: true. This all works.
When I try to label the markers, the labels only display when I'm zoomed far out. When I zoom in the labels disappear "underground", which is under the Terrain "layer".
How to solve this? I've looked at this, but it didn't help in my case. This neither.
The code:
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
timeline: false, // Hide clock thing
animation: false, // ditto
});
// Add Cesium OSM Buildings, a global 3D buildings layer.
const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());
// Fly the camera to the given longitude, latitude, and height.
viewer.camera.flyTo({
destination : Cesium.Cartesian3.fromDegrees(11.952996, 57.671910, 400),
orientation : {
heading : Cesium.Math.toRadians(0.0),
pitch : Cesium.Math.toRadians(-15.0),
}
});
var promise = Cesium.GeoJsonDataSource.load('data/botaniska_play.geojson', {
clampToGround: true,
markerColor: Cesium.Color.DARKGREEN,
});
promise.then (function (dataSource) {
viewer.dataSources.add (dataSource);
var entities = dataSource["_entityCollection"]["_entities"]["_array"];
entities.forEach (entity => {
// Add (and poistion?) label
entity.label = new Cesium.LabelGraphics ({
text: entity['_properties']['art']._value,
//eyeOffset: new Cesium.Cartesian3 (0, 10, 0),
horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0.0, -40.0), // Show above ground?
});
});
});
Snippet of the geojson:
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[11.9531552705143,57.6814657645123]},"properties":{"ogc_fid":6,"geom":"{\"type\":\"Point\",\"coordinates\":[11.9531552705143,57.6814657645123]}","plats":"Test Plats","besikt_datum":"2013-06-07","trad_id":"6","art":"Magnolia stellata","status":null,"aldersfas":"Gammalt","hojd":9,"krondiameter":9,"stamdiameter":56,"skador":"Lindriga","vitalitet":"God","anmarkningar":"Vackert","rekommendationer":null,"risk":"Låg","resterande_risk":"-","atgards_datum":null,"atgard_gjort":null,"nasta_besiktning":"2014-06-07","stabiliserings_datum":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[11.9500765558289,57.6815427053798]},"properties":{"ogc_fid":21,"geom":"{\"type\":\"Point\",\"coordinates\":[11.9500765558289,57.6815427053798]}","plats":null,"besikt_datum":"2017-06-09","trad_id":null,"art":"Acer griseum","status":null,"aldersfas":"Vuxet","hojd":9,"krondiameter":8,"stamdiameter":50,"skador":null,"vitalitet":"God","anmarkningar":"Alléträd högt naturvärde\n","rekommendationer":null,"risk":"Låg","resterande_risk":null,"atgards_datum":null,"atgard_gjort":null,"nasta_besiktning":null,"stabiliserings_datum":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[11.9514171990732,57.6828566964894]},"properties":{"ogc_fid":3,"geom":"{\"type\":\"Point\",\"coordinates\":[11.9514171990732,57.6828566964894]}","plats":"Test Plats","besikt_datum":"2013-06-07","trad_id":"3","art":"Populus siemonii","status":null,"aldersfas":"Gammalt","hojd":15,"krondiameter":14,"stamdiameter":60,"skador":"Inga","vitalitet":"God","anmarkningar":"Fin","rekommendationer":null,"risk":"Låg","resterande_risk":"-","atgards_datum":null,"atgard_gjort":null,"nasta_besiktning":"2014-06-07","stabiliserings_datum":null}} ...
Try add disableDepthTestDistance: Number.POSITIVE_INFINITY to LabelGraphics options
I have used Threejs to load FBX 3D model in scene. Due to different scale of the 3D model, it looks different in viewer.
See the photo:
Proper FBX with scale 1 : Download FBX MODEL 1
Problematic FBX with scale 0.1 : Download FBX MODEL 2
Both models should look in same scale, dimension, position and rotation of 3D model?
How to handle Camera?
How to handle lights? (Currently, there are 2 lights in the viewer.)
Here is the demo link for code review:
let fbxLoader = new FBXLoader();
fbxLoader.load(
objectUrl, //1
object => {
scene.add(object);
object.traverse(it => {
if (it.isMesh) {
it.receiveShadow = true;
it.castShadow = true;
scope.os = object.scale;
}
})
let boxHelper = new THREE.Box3Helper(new THREE.Box3());
boxHelper.visible = true;
boxHelper.box.setFromObject(object);
plane.position.y = boxHelper.box.min.y;
},
xhr => {
// console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
// called when loading has errors
error => {
console.log("An error happened" + error);
}
);
My JsFiddle Link : https://jsfiddle.net/43eqnp2f/
I'm working with a sensor to control the rotation of a hand model in THREE. I want to be able to apply an offset (on button click) to the hand model to adjust for any initial sensor position discrepancies.
I'm using quaternions and i'm not entirely clear on how to do this.
It seems that the approach should be to get the current position, get the difference between the current position and the home position [0,0,0,1], and apply this offset to the live sensor values.
I've tried this so far:
// on button click
quaternionOffset.copy(quaternion.multiplyQuaternions(quaternion.inverse(), startPosition))
// on update
quaternion.multiply(quaternionOffset)
initial sensor position (example)
home position (fingers pointing forward)
Thank you for the help
I think your approach is correct, just make sure you don't accidentally apply the home quaternion inside the input quaternion :
var inputQuaternion = new THREE.Quaternion();
var homeQuaternion = new THREE.Quaternion();
document.addEventListener('click', function () {
// set the current rotation as home
homeQuaternion.copy(inputQuaternion).inverse();
});
function updateFromSensor(quaternion) {
inputQuaternion.copy(quaternion);
object.quaternion.multiplyQuaternions(inputQuaternion, homeQuaternion);
}
let baseQuaternion = new THREE.Quaternion();
let diff;
function changeHandler(value) {
if (!diff) {
diff = baseQuaternion.multiply(value.inverse());
}
let calibrated = new THREE.Quaternion();
calibrated.multiplyQuaternions(diff, value);
object.setRotationFromQuaternion(calibrated);
}
Inspired by https://stackoverflow.com/a/22167097/1162838