16

I have an STL file loaded into my scene with a single colour applied to a phong material

I'd like a way of applying two colours to this mesh's material with a gradient effect applied on the Z axis a like the example below.Gradient Vase]1

I have a feeling I may have to introduce shaders but I've not gotten this far with three.js.

Huskie69
  • 795
  • 3
  • 11
  • 31

3 Answers3

58

Simple gradient shader, based on uvs:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(13, 25, 38);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
  antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);

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

var geometry = new THREE.CylinderBufferGeometry(2, 5, 20, 32, 1, true);
var material = new THREE.ShaderMaterial({
  uniforms: {
    color1: {
      value: new THREE.Color("red")
    },
    color2: {
      value: new THREE.Color("purple")
    }
  },
  vertexShader: `
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 color1;
    uniform vec3 color2;
  
    varying vec2 vUv;
    
    void main() {
      
      gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
    }
  `,
  wireframe: true
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);



render();

function resize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}

function render() {
  if (resize(renderer)) {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
  display;
  block;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/controls/OrbitControls.js"></script>

Simple gradient shader, based on coordinates:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(13, 25, 38);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
  antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);

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

var geometry = new THREE.CylinderBufferGeometry(2, 5, 20, 16, 4, true);
geometry.computeBoundingBox();
var material = new THREE.ShaderMaterial({
  uniforms: {
    color1: {
      value: new THREE.Color("red")
    },
    color2: {
      value: new THREE.Color("purple")
    },
    bboxMin: {
      value: geometry.boundingBox.min
    },
    bboxMax: {
      value: geometry.boundingBox.max
    }
  },
  vertexShader: `
    uniform vec3 bboxMin;
    uniform vec3 bboxMax;
  
    varying vec2 vUv;

    void main() {
      vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 color1;
    uniform vec3 color2;
  
    varying vec2 vUv;
    
    void main() {
      
      gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
    }
  `,
  wireframe: true
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);



render();

function resize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}

function render() {
  if (resize(renderer)) {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/controls/OrbitControls.js"></script>

Gradient with vertex colours:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
  antialias: true
});
var canvas = renderer.domElement
document.body.appendChild(canvas);


var geom = new THREE.TorusKnotGeometry(2.5, .5, 100, 16);

var rev = true;

var cols = [{
  stop: 0,
  color: new THREE.Color(0xf7b000)
}, {
  stop: .25,
  color: new THREE.Color(0xdd0080)
}, {
  stop: .5,
  color: new THREE.Color(0x622b85)
}, {
  stop: .75,
  color: new THREE.Color(0x007dae)
}, {
  stop: 1,
  color: new THREE.Color(0x77c8db)
}];

setGradient(geom, cols, 'z', rev);

function setGradient(geometry, colors, axis, reverse) {

  geometry.computeBoundingBox();

  var bbox = geometry.boundingBox;
  var size = new THREE.Vector3().subVectors(bbox.max, bbox.min);

  var vertexIndices = ['a', 'b', 'c'];
  var face, vertex, normalized = new THREE.Vector3(),
    normalizedAxis = 0;

  for (var c = 0; c < colors.length - 1; c++) {

    var colorDiff = colors[c + 1].stop - colors[c].stop;

    for (var i = 0; i < geometry.faces.length; i++) {
      face = geometry.faces[i];
      for (var v = 0; v < 3; v++) {
        vertex = geometry.vertices[face[vertexIndices[v]]];
        normalizedAxis = normalized.subVectors(vertex, bbox.min).divide(size)[axis];
        if (reverse) {
          normalizedAxis = 1 - normalizedAxis;
        }
        if (normalizedAxis >= colors[c].stop && normalizedAxis <= colors[c + 1].stop) {
          var localNormalizedAxis = (normalizedAxis - colors[c].stop) / colorDiff;
          face.vertexColors[v] = colors[c].color.clone().lerp(colors[c + 1].color, localNormalizedAxis);
        }
      }
    }
  }
}

var mat = new THREE.MeshBasicMaterial({
  vertexColors: THREE.VertexColors,
  wireframe: true
});
var obj = new THREE.Mesh(geom, mat);
scene.add(obj);



render();

function resize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}

function render() {
  if (resize(renderer)) {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  renderer.render(scene, camera);
  obj.rotation.y += .01;
  requestAnimationFrame(render);
}
html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
  display;
  block;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.min.js"></script>

Actually, it's up to you which approach to use: shaders, vertex colours, textures etc.

prisoner849
  • 16,894
  • 4
  • 34
  • 68
  • Oh this is perfect, I think the gradient shader based on uvs is sufficient but I've got something to play with now and might try it out with vertex colours if I need it - thank you!! – Huskie69 Oct 02 '18 at 20:01
  • I've tried the first method against a loader model (STL file) and it's only applying the first colour across the entire mesh. Does this work with loader objects? – Huskie69 Oct 02 '18 at 20:34
  • 1
    @Huskie69 If an object has uvs yes. You can pass a bounding box to the shaders and use it with coordinates of vertices of the object. – prisoner849 Oct 02 '18 at 21:01
  • 1
    @Huskie69 I've updated the answer with the approach of using `.boundingBox` values of `.min` and `.max` in uniforms. – prisoner849 Oct 02 '18 at 21:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/181210/discussion-between-huskie69-and-prisoner849). – Huskie69 Oct 03 '18 at 10:26
  • @prisoner849, how can we do this with 3 colors? Say from red to blue to green? – Halo Apr 20 '22 at 22:00
  • 1
    @Anye The easiest way is to use a texture with gradient. – prisoner849 Apr 21 '22 at 04:12
  • Great, thanks!! – Halo Apr 21 '22 at 11:52
  • Hi, your examples are very nice, but I can't apply them to single faces of a solid (BoxGeometry), how should I do? Thank you – Angelo Vit May 21 '23 at 09:37
9

If you want to keep the functionality of the MeshPhongMaterial you can try extending the material.

This is a somewhat broad topic with several approaches, and you can read more about it in depth here.

There is a line in the phong materials shader that looks like this

vec4 diffuseColor = vec4( diffuse, opacity );

So after studying the book of shaders or some other tutorials, you will learn that you can mix two colors by using a normalized factor ( a number between 0,1).

That means that you could change this line to something like this

vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(myFactor)), opacity);

You can extend the shader as such

const myFactor = { value: 0 }
const myColor = {value: new THREE.Color}


myMaterial.onBeforeCompile = shader=>{
  shader.uniforms.myFactor = myFactor
  shader.uniforms.myColor = myColor
  shader.fragmentShader = `
  uniform vec3 myColor;
  uniform float myFactor;
  ${shader.fragmentShader.replace(
    vec4 diffuseColor = vec4( diffuse, opacity );
    vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(myFactor)), opacity);
  )}
`

Now when you change myFactor.value the color of your object should change from myMaterial.color to myColor.value.

Now to actually make it into a gradient you would replace myFactor with something dynamic. I like prisoners solution to use the uvs. It's entirely done in javascript, and very simple to hook up in this shader. Other approaches would probably require more shader work.

vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(vUv.y)), opacity);

Now the problem you may encounter - if you call new PhongMaterial({color}) ie. without any textures provided to it, the shader will compile without vUv. There are many conditions that would cause it to compile and be useful to you, but i'm not sure if they break other stuff:

#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP ) || defined( USE_EMISSIVEMAP ) || defined( USE_ROUGHNESSMAP ) || defined( USE_METALNESSMAP )

So, adding something like

myMaterial.defines = {USE_MAP:''} 

Might make vUv variable available for your shader. This way you get all the lights of the phong material to affect the material, you just change the base color.

pailhead
  • 5,162
  • 2
  • 25
  • 46
4

If you want your gradient to be static, you could just add a texture to your material using the .map property. Or you could assign it to the .emissiveMap property if you want it to "glow" without the need of lights.

However, if you want your gradient to change, and always fade in the z-axis, even after rotating the model or camera, you'd have to write a custom shader, which would require you to take some tutorials. You could look at this example for how to implement custom shaders in Three.js, and visit https://thebookofshaders.com/ to get a good understanding on how to write a simple gradient shader.

M -
  • 26,908
  • 11
  • 49
  • 81