将颜色渐变应用于网格上的材质 - three.js

16

我在场景中加载了一个STL文件,并为Phong材质应用了单一颜色。

我想找到一种方法,可以对这个网格的材质应用两种颜色,并在Z轴上应用渐变效果,就像下面的例子。Gradient Vase]1

我觉得我可能需要引入着色器,但我还没有在three.js中做到这一点。

3个回答

60

基于UV的简单渐变着色器:

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>

基于坐标的简单渐变着色器:

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>

渐变与顶点颜色:

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>

实际上,你可以选择使用哪种方式:着色器、顶点颜色、纹理等。


哦,这太完美了,我认为基于UV的渐变着色器已经足够了,但现在我有东西可以玩耍了,如果需要的话,我可能会尝试使用顶点颜色 - 谢谢! - Huskie69
我已经尝试了第一种方法来处理一个装载器模型(STL文件),但它只是把第一种颜色应用到整个网格上。这对于装载器对象有效吗? - Huskie69
1
如果一个对象有UV,那么可以将边界框传递给着色器,并与对象的顶点坐标一起使用。 - prisoner849
1
@Huskie69 我已经更新了答案,使用.boundingBox中的.min.max值来进行统一处理。 - prisoner849
1
@Anye 最简单的方法是使用带有渐变的纹理。 - prisoner849
显示剩余4条评论

9
如果您想保留MeshPhongMaterial的功能,可以尝试扩展该材质。
这是一个相当广泛的主题,有几种方法可供选择,您可以在此处深入了解更多信息。 phong材质着色器中有一行代码看起来像这样。
vec4 diffuseColor = vec4( diffuse, opacity );

在学习《着色器之书》或其他教程后,您会了解到可以通过使用规范化因子(介于0,1之间的数字)混合两种颜色。

这意味着您可以将此行更改为以下内容:

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

你可以像这样扩展着色器

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);
  )}
`

现在当你改变myFactor.value时,你的对象的颜色应该从myMaterial.color变为myColor.value
现在要实际将其制作成渐变,您需要用动态方式替换myFactor。我喜欢使用UV的囚犯解决方案。它完全是用JavaScript完成的,并且非常容易在此着色器中连接。其他方法可能需要更多的着色器工作。
vec4 diffuseColor = vec4( mix(diffuse, myColor, vec3(vUv.y)), opacity);

现在可能会遇到的问题是 - 如果您调用new PhongMaterial({color}),即没有提供任何纹理,则着色器将在没有vUv的情况下编译。 有许多条件会导致它编译并对您有用,但我不确定它们是否会破坏其他东西:
#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 )

因此,添加类似以下内容:

myMaterial.defines = {USE_MAP:''} 

可能会使vUv变量在您的着色器中可用。这样,您就可以让冯氏材质的所有光线影响材质,只需更改基础颜色即可。

2
我已将该问题标记为收藏夹,因为你的回答。非常有启发性。 - prisoner849
又是一份出色而且非常有用的回答,谢谢! - Huskie69
1
在最新版本(当前为r111)中,他们添加了USE_UV:''以启用UV。myMaterial.defines = {USE_MAP:''} - prisoner849

4
如果您想让渐变效果保持静态,可以使用.map属性将纹理添加到材质中。或者,如果您想要它在没有灯光的情况下“发光”,则可以将其分配给.emissiveMap属性。
但是,如果您想让渐变效果改变,并且始终沿z轴淡入,即使旋转模型或相机,您需要编写自定义着色器,这需要您参考一些教程。您可以查看此示例了解如何在Three.js中实现自定义着色器,并访问https://thebookofshaders.com/以了解如何编写简单渐变着色器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接