Custom Phong Shader with USE_NORMALMAP - three.js

I've created a simple Phong based custom shader which creates the opacity from luminosity. Now when I'm trying to add Normal Map functionality WebGL is throwing me errors.
var vertexShader = THREE.ShaderLib.phong.vertexShader;
var fragmentShader = getShader();
var uniforms = THREE.UniformsUtils.clone(THREE.ShaderLib.phong.uniforms);
var defines = {};
defines[ "USE_MAP" ] = "";
defines[ "USE_NORMALMAP" ] = "";
var color_map = loader.load( 'img/colored.jpg' );
var normal_map = loader.load( 'img/skin_normal.jpg' );
uniforms.map = { type: "t", value: color_map};
uniforms.normalMap = { type: "t", value: normal_map};
uniforms.normalScale = { type: "v2", value: new THREE.Vector2( 0.25, 0.25 )};
If if I comment out the USE_NORMALMAP everything works as expected (without normal map obviously) but if it's enabled I'm getting this:
three.js:16058 THREE.WebGLShader: Shader couldn't compile.Gt # three.js:16058Kt # three.js:16578acquireProgram # three.js:16996w # three.js:21057E # three.js:21214renderBufferDirect # three.js:20163b # three.js:20986render # three.js:20727render # VREffect.js:396value # a-scene.js:425(anonymous function) # bind.js:12
three.js:16612 THREE.WebGLProgram: shader error: 0 gl.VALIDATE_STATUS false gl.getProgramInfoLog invalid shaders ERROR: 0:684: 'GL_OES_standard_derivatives' : extension is disabled
ERROR: 0:685: 'GL_OES_standard_derivatives' : extension is disabled
ERROR: 0:686: 'GL_OES_standard_derivatives' : extension is disabled
ERROR: 0:687: 'GL_OES_standard_derivatives' : extension is disabled
I'm assuming it's because I'm not loading something in the shader or passing the normalMap and normalScale somehow in wrong format. What could be causing this?

Related

A-Frame & Three.js: Color map makes object white

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.

Applying two different fragment shaders to two different materials (of the same type) using onBeforeCompile?

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.

A-Frame THREE.TextureLoader loading texture whice is looking whitewashed

I am updating an a-image scr using THREE.TextureLoader
let loader = new THREE.TextureLoader()
const imgload = loader.load(
'./test.png',
function ( texture ) {
firstFrameImage.getObject3D('mesh').material.map = texture
firstFrameImage.getObject3D('mesh').material.needsUpdate = true
},
// onProgress callback currently not supported
undefined,
// onError callback
function ( err ) {
console.error( 'An error happened.' );
}
)
Its updating the texture but its making the texture whitewashed. Can any one help?
Original image:
original
Updated texture coming as:
after update
Try to fix this by doing this:
texture.encoding = THREE.sRGBEncoding;
Color deviations like this mostly occur because of wrong color space definitions.

Setting ShaderMaterial `skinning` property to `true` causes 'too many uniforms' error

I have the following function which loads a mesh from a JSON file:
loadJSONModel(filename, modelName) {
let loader = new THREE.JSONLoader();
loader.load(`assets/${filename}`, (geometry, materials) => {
let material = Shader.createShaderMaterial(Shader.LINEAR_BLEND_SKINNING_VERT, Shader.BASIC_FRAG);
let mesh = new THREE.SkinnedMesh(geometry, material);
mesh.material.skinning = true;
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
this.scene.add(mesh);
});
where the Shader.createShaderMaterial function does the following:
static createShaderMaterial(vertex, fragment, uniforms) {
if (uniforms === undefined) uniforms = {};
return new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertex,
fragmentShader: fragment,
});
}
And now I keep getting this dastardly error:
THREE.WebGLProgram: shader error: 0 gl.VALIDATE_STATUS false gl.getProgramInfoLog invalid shaders ERROR: too many uniforms
which goes away if I don't do mesh.material.skinning = true, but of course, I need the skinning flag to be set in the shader, so I need that there.
My problem doesn't seem to be the same as others I've found thus far through Google. I'm not reusing my geometry from another mesh. I'm constructing a SkinnedMesh too, not any old mesh. My setup can support 1024 uniforms. I'm baffled. Any help would be much appreciated.
Of course it had to be something stupid like the fact that the ThreeJS WebGLProgram was setting the MAX_BONES macro in my shader file to 1024, and I was using that value in my shader to initialize this uniform: uniform mat4 boneMatrices[MAX_BONES], and my system only supported 1024 uniforms max.
Now the question is why the allocateBones function of THREE.WebGLPrograms does this:
function allocateBones( object ) {
if ( capabilities.floatVertexTextures && object && object.skeleton && object.skeleton.useVertexTexture ) {
return 1024;
} else {
// default for when object is not specified
// ( for example when prebuilding shader to be used with multiple objects )
//
// - leave some extra space for other uniforms
// - limit here is ANGLE's 254 max uniform vectors
// (up to 54 should be safe)
var nVertexUniforms = capabilities.maxVertexUniforms;
var nVertexMatrices = Math.floor( ( nVertexUniforms - 20 ) / 4 );
var maxBones = nVertexMatrices;
if ( object !== undefined && (object && object.isSkinnedMesh) ) {
maxBones = Math.min( object.skeleton.bones.length, maxBones );
if ( maxBones < object.skeleton.bones.length ) {
console.warn( 'WebGLRenderer: too many bones - ' + object.skeleton.bones.length + ', this GPU supports just ' + maxBones + ' (try OpenGL instead of ANGLE)' );
}
}
return maxBones;
}
}
ie. why does satisfying capabilities.floatVertexTextures && object && object.skeleton && object.skeleton.useVertexTexture entail that MAX_BONES is automatically 1024. But that is another question, and one which I am not particularly concerned about at this point. So I've decided to just use a RawShaderMaterial and manually set my MAX_BONES macro to an appropriate value.

Three.js ShaderMaterial issue with lights

Helo, here's part of my code - earth globe :)
function createGlobe(){
var normalMap = THREE.ImageUtils.loadTexture("images/earth_normal_2048.jpg");
var surfaceMap = THREE.ImageUtils.loadTexture("images/earth_surface_2048.jpg");
var specularMap = THREE.ImageUtils.loadTexture("images/earth_specular_2048.jpg");
var shader = THREE.ShaderLib["phong"];
var uniforms = THREE.UniformsUtils.clone(shader.uniforms);
uniforms["tNormal"] = {type:"t", value:normalMap};
uniforms["tDiffuse"] = {type:"t", value:surfaceMap};
uniforms["tSpecular"] = {type:"t", value:specularMap};
//uniforms["enableDiffuse"] = true;
//uniforms["enableSpecular"] = true;
var shaderMaterial = new THREE.ShaderMaterial({
fragmentShader:shader.fragmentShader,
vertexShader:shader.vertexShader,
uniforms:uniforms,
lights:false
});
var globeGeometry = new THREE.SphereGeometry(1,32,32);
//tangents are needed for the shader
globeGeometry.computeTangents();
globe = new THREE.Mesh(globeGeometry, shaderMaterial);
globe.rotation.z = 0.41;
earthgroup.add(globe);
}
The problematic spot is at ShaderMaterial parameter "lights:true", when set there is following error
Uncaught TypeError: Cannot set property 'value' of undefined three.js:23875
refreshUniformsLights three.js:23875
setProgram three.js:23593
renderBuffer three.js:22018
renderObjects three.js:22689
render three.js:22563
render Earth-clouds.html:100
wrappedCallback
which is
function refreshUniformsLights ( uniforms, lights ) {
uniforms.ambientLightColor.value = lights.ambient;
....
}
and that seems to me that lights are not defined? Well I got them.
When I set lights:false it renders the globe with the default material. It looks like a bug that could by in my code :-)

Resources