1

I have an .obj file loaded with THREE.OBJLoader in my three.js scene.

I'd like to apply a linear color gradient on the z-axis onto this object while preserving MeshStandardMaterial shaders. An example of a 2 color linear gradient applied onto an object is below.

I need to do this while preserving the shaders that come with the MeshStandardMaterial class. I think to do this, I need to inject the vertexShader and materialShaders of MeshStandardMaterial with some code.

This is partially achieved in the answers to this question. These answers create a new shader material without the properties of the MeshStandardMaterial, which I neeed.

I tried using onBeforeCompile but I don't know where to inject the code that is outlined in the answers to the question I mentioned above. I only have a basic understanding of shaders.

The material of my object is defined by the THREE.MeshStandardMaterial class. I set the following properties of the class: metalness, roughness, transparent and opacity. I set the map and envmap properties with textures.

2 Answers2

4

It's hard to realize what is what in shader chunks, but not impossible.

body{
  overflow: hidden;
  margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.121.1/build/three.module.js";
import {OrbitControls} from "https://cdn.jsdelivr.net/npm/three@0.121.1/examples/jsm/controls/OrbitControls.js";

let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 10, 20);
let renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

let controls = new OrbitControls(camera, renderer.domElement);

let light = new THREE.DirectionalLight(0xffffff, 1);
light.position.setScalar(10);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 1));

let g = new THREE.TorusKnotBufferGeometry(5, 1, 128, 16);
g.rotateX(-Math.PI * 0.5);
g.computeBoundingBox();

let uniforms = {
    bbMin: {value: g.boundingBox.min},
  bbMax: {value: g.boundingBox.max},
  color1: {value: new THREE.Color(0xff0000)},
  color2: {value: new THREE.Color(0xffff00)}
}
console.log(g);
let m = new THREE.MeshStandardMaterial({
    roughness: 0.25,
  metalness: 0.75,
  map: new THREE.TextureLoader().load("https://threejs.org/examples/textures/floors/FloorsCheckerboard_S_Diffuse.jpg", tex => {
    tex.wrapS = THREE.RepeatWrapping;
    tex.wrapT = THREE.RepeatWrapping;
    tex.repeat.set( 16, 1 );
  }),
  onBeforeCompile: shader => {
    shader.uniforms.bbMin = uniforms.bbMin;
    shader.uniforms.bbMax = uniforms.bbMax;
    shader.uniforms.color1 = uniforms.color1;
    shader.uniforms.color2 = uniforms.color2;
    shader.vertexShader = `
        varying vec3 vPos;
      ${shader.vertexShader}
    `.replace(
    `#include <begin_vertex>`,
    `#include <begin_vertex>
    vPos = transformed;
    `
    );
    shader.fragmentShader = `
        uniform vec3 bbMin;
      uniform vec3 bbMax;
      uniform vec3 color1;
      uniform vec3 color2;
      varying vec3 vPos;
      ${shader.fragmentShader}
    `.replace(
        `vec4 diffuseColor = vec4( diffuse, opacity );`,
      `
      float f = clamp((vPos.z - bbMin.z) / (bbMax.z - bbMin.z), 0., 1.);
      vec3 col = mix(color1, color2, f);
      vec4 diffuseColor = vec4( col, opacity );`
    );
    console.log(shader.vertexShader);
    console.log(shader.fragmentShader);
  }
});

let o = new THREE.Mesh(g, m);
scene.add(o);


renderer.setAnimationLoop(()=>{
    renderer.render(scene, camera);
});
</script>
prisoner849
  • 16,894
  • 4
  • 34
  • 68
  • Thank you for your answer. You are really a wizard with shaders. May I ask what resources I would need to go through to learn about shaders to be able to do what you do in this answer? – Lanky Panky Oct 29 '20 at 19:46
  • A follow up question, would it be possible to edit this code so the gradient applies to the emissiveMap instead of the map property? – Lanky Panky Oct 30 '20 at 16:23
  • @LankyPanky If you want it like that, why do you need MeshStandardMaterial? As you want to apply that gradient as emissive color. It's enough to have MeshBasicMaterial. – prisoner849 Oct 30 '20 at 17:56
  • I don't know the difference between those two except that one is PBR based and that there is a computation tradeoff. I fear I might've not formulated my question perfectly. To give more context I've created this question on which I would love your take: https://stackoverflow.com/questions/64615943/how-to-best-represent-graded-lenses – Lanky Panky Oct 30 '20 at 21:22
1

The MeshStandardMaterial shader compiles into hundreds of lines of code, so it would be very difficult to pinpoint exactly where to change the colors while maintaining environment reflections, lighting, metalness, roughness, and all those behaviors. You might be overthinking this. If all you need is a gradient on a MeshStandardMaterial, why don't you just create a texture with your gradient, then assign it to the Material.map property? If you add a white AmbientLight, you should be able to see the gradient with no problem.

Alternatively, if you want the gradient to not be affected by lights, you could assign the texture to the material's .emissive property.

Lastly, if you want to avoid a texture, you could assign a color to each vertex, then set Material.vertexColors to true.

M -
  • 26,908
  • 11
  • 49
  • 81
  • Thank you for your answer. I did not consider using a canvas since I already have a map property and don't know how overlay 2 maps on top of each other grammatically, but experimenting a little bit with a canvas texture right now, I see how it can be very useful to go that way. – Lanky Panky Oct 29 '20 at 19:48