three.js初体验(材质Material)

369 阅读8分钟

材质简单理解就是设置几何体各个面的颜色。但它不是单纯的颜色,它能模拟在不同光照下颜色的表现。比如太阳光照射光滑的物体,表面会出现白色的反光,都能模拟。材质和渲染器无关,在开发中定义一份材质就可以重复使用。
定义材质的常用的方式有两种:

  1. 在实例化时传入要配置的参数
const material = new THREE.MeshBasicMaterial({
    color: 0xFF0000, // 也可以使用CSS的颜色字符串
});
  1. 通过材质的方法设置属性
const material = new THREE.MeshBasicMaterial();
material.color.setHSL(0, 1, 0.5); 
material.color.set(0x00FFFF); // 同 CSS的 #RRGGBB 风格

MeshBasicMaterial基础材质

  • 以简单着色的方式实现
  • 不受灯光的影响
// 基础材质
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
const sphereMaterial = new THREE.MeshBasicMaterial({color: 0xf37e7d});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// 位置
sphere.position.x = 0;
sphere.position.y = 4;
sphere.position.z = 2;
// 阴影
sphere.castShadow = true;
scene.add(sphere);

// 基础材质
const sphereGeometry1 = new THREE.SphereGeometry(4, 20, 20);
// wireframe 基础材质的属性,设置true,只渲染线框
const sphereMaterial1 = new THREE.MeshBasicMaterial({color: 0xf37e7d, wireframe: true});
const sphere1 = new THREE.Mesh(sphereGeometry1, sphereMaterial1)
sphere1.position.x = 10;
sphere1.position.y = 4;
sphere1.position.z = 0;
sphere1.castShadow = true;
scene.add(sphere1);

运行效果:

demo1.png

MeshLambertMaterial Lambert网格材质

  • 表面光滑的材质
  • 受灯光的影响,不过只在顶点计算光照
  • 能很好的模拟一些表面(例如未经处理的木材或石材)。因为只在顶点计算光照,不能模拟具有镜面高光的表面(如地板砖这些)
// 基础材质
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
const sphereMaterial = new THREE.MeshLambertMaterial({color: 0xf37e7d});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// 位置
sphere.position.x = 0;
sphere.position.y = 4;
sphere.position.z = 2;
// 阴影
sphere.castShadow = true;
scene.add(sphere);

这里灯光用的方向光黑色,不设置灯光几何体就会展示为全黑。几何体最后展示的颜色是,灯光颜色乘以材质的颜色来展示的。
运行效果:

demo2.png

MeshPhongMaterial Phong网格材质

  • 具有镜面高光的材质
  • 每个像素都会计算光照
  • 模拟具有镜面高光的表面(如地板砖)
// 基础材质
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
// shininess属性,决定高光的光泽,值越大光泽越亮。默认是30
const sphereMaterial = new THREE.MeshPhongMaterial({color: 0xf37e7d, shininess: 50});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// 位置
sphere.position.x = 0;
sphere.position.y = 4;
sphere.position.z = 2;
// 阴影
sphere.castShadow = true;
scene.add(sphere);

运行效果:

demo3.png

基于深度着色的MeshDepthMaterial

使用这种材质的物体,其外观不是由光照或某个材质属性决定的;而是由物体到相机的距离决定的。可以将这种材质与其他材质相结合,从而很容易地创建出逐渐消失的效果。

const cubeGeometry = new THREE.CubeGeometry(20, 20, 20);
const cubeMaterial = new THREE.MeshDepthMaterial();
for (let i = 0; i < 300; i++) {
    const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.position.x = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.position.y = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.position.z = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.rotation.x = Math.random() * Math.PI;
    cube.rotation.y = Math.random() * Math.PI;
    cube.rotation.z = Math.random() * Math.PI;
    cube.updateMatrix();
    cube.add(cube);
}

注意:发现80+的版本不支持这个材质,全部都是黑色,换了一个60的版本的 three.js 文件,就没问题了。
运行效果(初始颜色设置成黑色,黑色好看):

demo4.png

融合材质

MeshDepthMaterial 不能设置颜色,一切都是由材质的默认属性决定的。但是,three.js 可通过联合材质创建出新效果。

const cubeGeometry = new THREE.CubeGeometry(20, 20, 20);
const cubeMaterial = new THREE.MeshDepthMaterial();
// 为了使颜色能够有渐变效果,必须设置MeshBasicMaterial的transparent为true,设置融合blending的方式
const cubeColorMaterial = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, blending: THREE.MultiplyBlending});
for (let i = 0; i < 300; i++) {
    // 这些方块可以从MeshDepthMaterial对象获得渐变效果,从MeshBasicMaterial获取颜色
    const sphere = new THREE.SceneUtils.createMultiMaterialObject(cubeGeometry, [cubeColorMaterial, sphereMaterial]);
    cube.position.x = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.position.y = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.position.z = 800 * ( 2.0 * Math.random() - 1.0 );
    cube.rotation.x = Math.random() * Math.PI;
    cube.rotation.y = Math.random() * Math.PI;
    cube.rotation.z = Math.random() * Math.PI;
    cube.updateMatrix();
    scene.add(cube);
}

运行效果:

demo5.png

为每个面指定材质的MeshFaceMaterial

通过 MeshFaceMaterial 可以为几何体的每一个面指定不同的材质。假如你有一个方块,上面有六个面,你可以用这种材质来为每个面指定一个材质。

const CubeGeometry = new THREE.CubeGeometry(5, 5, 5);
let mats = [];
mats.push(new THREE.MeshBasicMaterial({ color: 0x009e60 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0x0051ba }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffd500 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xff5800 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xc41e3a }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffffff }));
const CubeMaterial = new THREE.MeshFaceMaterial(mats);
const Cube = new THREE.Mesh(CubeGeometry, CubeMaterial);
Cube.position.x = 0;
Cube.position.y = 4;
Cube.position.z = 2;
Cube.castShadow = true;
scene.add(Cube);

运行效果:

demo6.png

使用ShaderMaterial创建自己的着色器

ShaderMaterialthree.js 库中功能最丰富、最复杂的一种材质。通过它,可以使用自己定制的着色器,直接在 WebGL 环境中运行。ShaderMaterial 包含的几个常用属性 wireframe、wireframeLinewidth、shading、vertexColors、fog。ShaderMaterial 还包含几个特别属性:

  • fragementShader (片元着色器):这个着色器定义的是每个传入的像素颜色
  • vertexShader (顶点着色器):这个着色器允许你修改每一个传入的顶点的位置
  • uniforms:通过这个属性可以向你的着色器发信息。同样的信息会发送到每一个顶点和片元
  • defines:这个属性可以转换为 vertexShaderfragmentShader 里的 #define 代码。该属性可以用来设置着色器程序里的一些全局变量
  • attributes:改属性可以修改每个顶点和片元。通常用来传递位置数据和法向量相关的数据。如果要用这个属性,anemia 你要为几何体中的所有顶点提供信息
  • lights:定义光照数据是否传递给着色器。默认为false

在创建ShaderMaterial,必须要传递两个重要的属性vertexShader和fragmentShader。两个都是对应的一段WebGL顶点和片元着色器源码字符串。
着色器代码是用的《three.js开发指南》里面的,想学习这个的同学可以看一下。

<script id="vertex-shader" type="x-shader/x-vertex">
  uniform float time;
  varying vec2 vUv;

  void main(){
    vec3 posChanged = position;
    posChanged.x = posChanged.x*(abs(sin(time*1.0)));
    posChanged.y = posChanged.y*(abs(cos(time*1.0)));
    posChanged.z = posChanged.z*(abs(sin(time*1.0)));
    //gl_Position = projectionMatrix * modelViewMatrix * vec4(position*(abs(sin(time)/2.0)+0.5),1.0);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
  }
</script>
<script id="fragment-shader-1" type="x-shader/x-fragment">
  precision highp float;
  uniform float time;
  uniform float alpha;
  uniform vec2 resolution;
  varying vec2 vUv;

  void main2(void)
  {
  vec2 position = vUv;
  float red = 1.0;
  float green = 0.25 + sin(time) * 0.25;
  float blue = 0.0;
  vec3 rgb = vec3(red, green, blue);
  vec4 color = vec4(rgb, alpha);
  gl_FragColor = color;
  }

  #define PI 3.14159
  #define TWO_PI (PI*2.0)
  #define N 68.5

  void main(void)
  {
  vec2 center = (gl_FragCoord.xy);
  center.x=-10.12*sin(time/200.0);
  center.y=-10.12*cos(time/200.0);

  vec2 v = (gl_FragCoord.xy - resolution/20.0) / min(resolution.y,resolution.x) * 15.0;
  v.x=v.x-10.0;
  v.y=v.y-200.0;
  float col = 0.0;

  for(float i = 0.0; i < N; i++)
  {
  float a = i * (TWO_PI/N) * 61.95;
  col += cos(TWO_PI*(v.y * cos(a) + v.x * sin(a) + sin(time*0.004)*100.0 ));
  }

  col /= 5.0;

  gl_FragColor = vec4(col*1.0, -col*1.0,-col*4.0, 1.0);
  }
</script>

<script id="fragment-shader-2" type="x-shader/x-fragment">
  // from http://glsl.heroku.com/e#7906.0

  uniform float time;
  uniform vec2 resolution;

  // 2013-03-30 by @hintz

  #define CGFloat float
  #define M_PI 3.14159265359

  vec3 hsvtorgb(float h, float s, float v)
  {
  float c = v * s;
  h = mod((h * 6.0), 6.0);
  float x = c * (1.0 - abs(mod(h, 2.0) - 1.0));
  vec3 color;

  if (0.0 <= h && h < 1.0)
  {
  color = vec3(c, x, 0.0);
  }
  else if (1.0 <= h && h < 2.0)
  {
  color = vec3(x, c, 0.0);
  }
  else if (2.0 <= h && h < 3.0)
  {
  color = vec3(0.0, c, x);
  }
  else if (3.0 <= h && h < 4.0)
  {
  color = vec3(0.0, x, c);
  }
  else if (4.0 <= h && h < 5.0)
  {
  color = vec3(x, 0.0, c);
  }
  else if (5.0 <= h && h < 6.0)
  {
  color = vec3(c, 0.0, x);
  }
  else
  {
  color = vec3(0.0);
  }

  color += v - c;

  return color;
  }

  void main(void)
  {

  vec2 position = (gl_FragCoord.xy - 0.5 * resolution) / resolution.y;
  float x = position.x;
  float y = position.y;

  CGFloat a = atan(x, y);

  CGFloat d = sqrt(x*x+y*y);
  CGFloat d0 = 0.5*(sin(d-time)+1.5)*d;
  CGFloat d1 = 5.0;

  CGFloat u = mod(a*d1+sin(d*10.0+time), M_PI*2.0)/M_PI*0.5 - 0.5;
  CGFloat v = mod(pow(d0*4.0, 0.75),1.0) - 0.5;

  CGFloat dd = sqrt(u*u+v*v);

  CGFloat aa = atan(u, v);

  CGFloat uu = mod(aa*3.0+3.0*cos(dd*30.0-time), M_PI*2.0)/M_PI*0.5 - 0.5;
  // CGFloat vv = mod(dd*4.0,1.0) - 0.5;

  CGFloat d2 = sqrt(uu*uu+v*v)*1.5;

  gl_FragColor = vec4( hsvtorgb(dd+time*0.5/d1, sin(dd*time), d2), 1.0 );
  }
</script>

<script id="fragment-shader-3" type="x-shader/x-fragment">
  uniform float time;
  uniform vec2 resolution;
  
  // tie nd die by Snoep Games.
  
  void main( void ) {
  
  vec3 color = vec3(1.0, 0., 0.);
  vec2 pos = (( 1.4 * gl_FragCoord.xy - resolution.xy) / resolution.xx)*1.5;
  float r=sqrt(pos.x*pos.x+pos.y*pos.y)/15.0;
  float size1=2.0*cos(time/60.0);
  float size2=2.5*sin(time/12.1);
  
  float rot1=13.00; //82.0+16.0*sin(time/4.0);
  float rot2=-50.00; //82.0+16.0*sin(time/8.0);
  float t=sin(time);
  float a = (60.0)*sin(rot1*atan(pos.x-size1*pos.y/r,pos.y+size1*pos.x/r)+time);
  a += 200.0*acos(pos.x*2.0+cos(time/2.0))+asin(pos.y*5.0+sin(time/2.0));
  a=a*(r/50.0);
  a=200.0*sin(a*5.0)*(r/30.0);
  if(a>5.0) a=a/200.0;
  if(a<0.5) a=a*22.5;
  gl_FragColor = vec4( cos(a/20.0),a*cos(a/200.0),sin(a/8.0), 1.0 );
  }
</script>

<script id="fragment-shader-4" type="x-shader/x-fragment">

  uniform float time;
  uniform vec2 resolution;

  vec2 rand(vec2 pos)
  {
  return
  fract(
  (
  pow(
  pos+2.0,
  pos.yx+2.0
  )*555555.0
  )
  );
  }

  vec2 rand2(vec2 pos)
  {
  return rand(rand(pos));
  }

  float softnoise(vec2 pos, float scale) {
  vec2 smplpos = pos * scale;
  float c0 = rand2((floor(smplpos) + vec2(0.0, 0.0)) / scale).x;
  float c1 = rand2((floor(smplpos) + vec2(1.0, 0.0)) / scale).x;
  float c2 = rand2((floor(smplpos) + vec2(0.0, 1.0)) / scale).x;
  float c3 = rand2((floor(smplpos) + vec2(1.0, 1.0)) / scale).x;

  vec2 a = fract(smplpos);
  return mix(mix(c0, c1, smoothstep(0.0, 1.0, a.x)),
  mix(c2, c3, smoothstep(0.0, 1.0, a.x)),
  smoothstep(0.0, 1.0, a.x));
  }

  void main( void ) {
  vec2 pos = gl_FragCoord.xy / resolution.y - time * 0.4;

  float color = 0.0;
  float s = 1.0;
  for (int i = 0; i < 6; ++i) {
  color += softnoise(pos + vec2(0.01 * float(i)), s * 4.0) / s / 2.0;
  s *= 2.0;
  }
  gl_FragColor = vec4(color,mix(color,cos(color),sin(color)),color,1);
  }
</script>

<script id="fragment-shader-5" type="x-shader/x-fragment">
  
  uniform vec2 resolution;
  uniform float time;

  vec2 rand(vec2 pos)
  {
  return fract( 0.00005 * (pow(pos+2.0, pos.yx + 1.0) * 22222.0));
  }
  vec2 rand2(vec2 pos)
  {
  return rand(rand(pos));
  }

  float softnoise(vec2 pos, float scale)
  {
  vec2 smplpos = pos * scale;
  float c0 = rand2((floor(smplpos) + vec2(0.0, 0.0)) / scale).x;
  float c1 = rand2((floor(smplpos) + vec2(1.0, 0.0)) / scale).x;
  float c2 = rand2((floor(smplpos) + vec2(0.0, 1.0)) / scale).x;
  float c3 = rand2((floor(smplpos) + vec2(1.0, 1.0)) / scale).x;

  vec2 a = fract(smplpos);
  return mix(
  mix(c0, c1, smoothstep(0.0, 1.0, a.x)),
  mix(c2, c3, smoothstep(0.0, 1.0, a.x)),
  smoothstep(0.0, 1.0, a.y));
  }

  void main(void)
  {
  vec2 pos = gl_FragCoord.xy / resolution.y;
  pos.x += time * 0.1;
  float color = 0.0;
  float s = 1.0;
  for(int i = 0; i < 8; i++)
  {
  color += softnoise(pos+vec2(i)*0.02, s * 4.0) / s / 2.0;
  s *= 2.0;
  }
  gl_FragColor = vec4(color);
  }
</script>

<script id="fragment-shader-6" type="x-shader/x-fragment">
  
  uniform float time;
  uniform vec2 resolution;

  void main( void )
  {

  vec2 uPos = ( gl_FragCoord.xy / resolution.xy );//normalize wrt y axis
  //suPos -= vec2((resolution.x/resolution.y)/2.0, 0.0);//shift origin to center

  uPos.x -= 1.0;
  uPos.y -= 0.5;

  vec3 color = vec3(0.0);
  float vertColor = 2.0;
  for( float i = 0.0; i < 15.0; ++i )
  {
  float t = time * (0.9);

  uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1;
  float fTemp = abs(1.0 / uPos.y / 100.0);
  vertColor += fTemp;
  color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 );
  }

  vec4 color_final = vec4(color, 1.0);
  gl_FragColor = color_final;
  }
</script>
//创建ShaderMaterial纹理的函数
function createMaterial(vertexShader, fragmentShader) {
    const vertShader = document.getElementById(vertexShader).innerHTML; //获取顶点着色器的代码
    const fragShader = document.getElementById(fragmentShader).innerHTML; //获取片元着色器的代码
    //配置着色器里面的attribute变量的值
    const attributes = {};
    //配置着色器里面的uniform变量的值
    const uniforms = {
        time: { type: "f", value: 0.2 },
        scale: { type: "f", value: 0.2 },
        alpha: { type: "f", value: 0.6 },
        resolution: {
            type: "v2",
            value: new THREE.Vector2(window.innerWidth, window.innerHeight),
        },
    };
    const meshMaterial = new THREE.ShaderMaterial({
        uniforms: uniforms,
        defaultAttributeValues: attributes,
        vertexShader: vertShader,
        fragmentShader: fragShader,
        transparent: true,
    });
    return meshMaterial;
}
//声明一个立方体几何图形
const cubeGeometry = new THREE.BoxGeometry(20, 20, 20)
//创建立方体六个面的纹理
const meshMaterial1 = createMaterial(
    "vertex-shader",
    "fragment-shader-1"
);
const meshMaterial2 = createMaterial(
    "vertex-shader",
    "fragment-shader-2"
);
const meshMaterial3 = createMaterial(
    "vertex-shader",
    "fragment-shader-3"
);
const meshMaterial4 = createMaterial(
    "vertex-shader",
    "fragment-shader-4"
);
const meshMaterial5 = createMaterial(
    "vertex-shader",
    "fragment-shader-5"
);
const meshMaterial6 = createMaterial(
    "vertex-shader",
    "fragment-shader-6"
);
const material = [
    meshMaterial6,
    meshMaterial5,
    meshMaterial4,
     meshMaterial3,
    meshMaterial2,
    meshMaterial1,
]
const cube = new THREE.Mesh(cubeGeometry, material);
scene.add(cube);

运行效果(虽然有点像理发店门口的,将就看一下):

demo7.png

demo8.png

总结一下

three.js 中越复杂的材质会消耗更多的 GPU 功耗。在绘制物体时我们就需要判断需要哪些材质功能,找到对应的材质以此来减少程序的消耗。