Web3D-材质篇

224 阅读7分钟

网帽材质 MeshMatcapMaterial

在场景没有光源的情况下,模拟出物体被光照的效果(该纹理是将光源、材质信息直接在建模软件中,烘培到纹理图上,渲染时不需要做额外的计算,性能提升明显)。

网帽材质 MeshMatcapMaterial 没有aoMap属性,但有 matcap 属性。

不对灯光作出反应。会投射阴影到接受阴影的物体上(and shadow clipping works),但不会产生自身阴影or接受阴影。

  // 创建立方体对象
  createObjects () {
    const colorTexture = this.textureLoader.load('/src/assets/textures/1.jpg') // 颜色贴图
    const aoTexture = this.textureLoader.load('/src/assets/textures/3.jpg') // AO贴图
    const normalTexture = this.textureLoader.load('/src/assets/textures/3.jpg') // 法线贴图

    const material = new THREE.MeshMatcapMaterial({
      // color: 0x1890ff,
      transparent: true,
      side: THREE.DoubleSide,
      matcap: aoTexture,
      map: colorTexture,
    })
    const mesh = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), material) // 立方体
    const meshMaterial = new THREE.MeshMatcapMaterial({
      // color: 0x1890ff,
      map: colorTexture,
      matcap: aoTexture,
      normalMap: normalTexture,
    })

    const box = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), meshMaterial) // 立方体

    mesh.position.x = -1
    box.position.x = 1

    this.scene.add(mesh, box)
    this.material = material;
    this.meshMaterial = meshMaterial;
  },
  // 调试工具
datGui () {
    const _this = this
    const gui = new dat.GUI();
    const params = {
      x: _this.meshMaterial.normalScale.x,
      y: _this.meshMaterial.normalScale.y,
    }

    // normalScale 是二维向量
    gui.add(params, 'x', 0, 1, 0.1).onChange(val => {
      _this.meshMaterial.normalScale = new THREE.Vector2(val, params.y) 
    })
    gui.add(params, 'y', 0, 1, 0.1).onChange(val => {
      _this.meshMaterial.normalScale = new THREE.Vector2(params.x, val ) 
    })
},

深度网格材质 MeshDepthMaterial

按深度绘制几何体的材质。深度基于相机远近平面。可用于观测几何体与相机的距离,几何体接近相机就是白色远离则是黑色

光照没有反应,适合被用来创建中的场景。

  // 创建立方体对象
  createObjects () {
    const geometry = new THREE.SphereGeometry(0.5, 64, 64)
    const material = new THREE.MeshDepthMaterial()
    const mesh1 = new THREE.Mesh(geometry, material)
    const mesh2 = new THREE.Mesh(geometry, material)
    const mesh3 = new THREE.Mesh(geometry, material)
    const mesh4 = new THREE.Mesh(geometry, material)

    mesh1.position.z = 0
    mesh2.position.z = 1
    mesh3.position.z = 2
    mesh4.position.z = 3

    this.scene.add( mesh1, mesh2, mesh3, mesh4 )
  },
  datGui () {
    const _this = this
    const gui = new dat.GUI();

    gui.add(_this.camera.position, 'z', 0.1, 10, 0.1).name('cameraZ')
  },

网格材质 MeshPhongMaterial

可模拟具有镜面高光的光泽表面(如,涂漆木材),即 没有高光贴图也能呈现的效果

该材质使用非物理Blinn-Phong模型来计算反射率,可对光照产生镜面反射

独特属性(继承自该材质,也会有这些属性)

emissive材质的放射(光)颜色,基本上不受其他光照影响的固有颜色。默认是黑色。

emissiveMap 设置放射(发光)贴图

emissiveIntensity 放射光强度。调节发光颜色,默认是1。

specular 指定该材质的光亮程度+其高光部分的颜色。若设置成跟color属性相同的颜色,将会得到更加类似金属的材质。若设置为灰色,材质将变得像塑料

shininess 指定高光部分的亮度,默认是30。

  // 创建立方体对象
  createObjects () {
    const colorTexture = this.textureLoader.load('/src/assets/textures/1.jpg')
    const normalTexture = this.textureLoader.load('/src/assets/textures/4.jpg')

    const material = new THREE.MeshPhongMaterial({
      transparent: true,
      side: THREE.DoubleSide,
      map: colorTexture
    })
    const box = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), material) // 球体
    const meshMaterial = new THREE.MeshPhongMaterial({
      map: colorTexture,
      // normalMap: normalTexture,
      specular: 0x00ffff, // 高光颜色
    })
    const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), meshMaterial) // 球体

    box.position.x = -1.5
    mesh.position.x = 1

    this.meshMaterial = meshMaterial
    this.material = material
    this.scene.add(box, mesh)
  },
  datGui () {
    const _this = this
    const gui = new dat.GUI();

    gui.add(_this.meshMaterial, 'shininess', 0, 10, 0.1) // 光照强度
    // 光照方位
    gui.add(_this.directionalLight.position, 'x', -10, 10, 0.1)
    gui.add(_this.directionalLight.position, 'y', -10, 10, 0.1)
    gui.add(_this.directionalLight.position, 'z', -10, 10, 0.1)
  },

卡通风格材质 MeshToonMaterial

二次元卡通风格,俗称3渲2。

Toon网格材质与Lambert材质很相似(是 MeshPhongMaterial卡通着色的扩展),但更偏向于卡通化,可使渐变层次更丰富

放大滤镜 magFilter 通过拉伸修复,这个非常小的渐变纹理贴图,该过程就使用了 mipmapping 映射,使其变得模糊。若想防止该情况,可设置纹理贴图的缩小滤镜。

minFilter 属性+ 放大滤镜 magFilter属性 为THREE.NearestFilter

渐变纹理贴图是非常小的渐变图片,可能只有1x3像素。

gradientMap 属性配置渐变。

const threeToneTexture = textureLoader.load('/public/textures/threeTone.jpg');
const fiveToneTexture = textureLoader.load('/public/textures/fiveTone.jpg');

const material = new THREE.MeshToonMaterial({
  color: 0xffffee,
  map: colorTexture,
  gradientMap: null,
});

/**
 * gui
 */
const gui = new dat.GUI();

gui.add(material, 'gradientMap', [
  'none', 'three', 'five',
]).onChange(val => {
  if (val === 'none') {
    material.gradientMap = null;
  } else if (val === 'three') {
    // 如果卡通效果失效, 明暗过度过于丝滑, 可能是因为梯度纹理过小, 需要调整 minFilter magFilter
    // threeToneTexture.minFilter = THREE.NearestFilter;
    threeToneTexture.magFilter = THREE.NearestFilter;
    
    // minFilter属性使用了NearestFilter, 可以按需为该纹理停用mipmapping,使得GPU不再处理其mip映射
    threeToneTexture.generateMipmaps = false; 
    
    material.gradientMap = threeToneTexture;
  } else {
    // fiveToneTexture.minFilter = THREE.NearestFilter;
    fiveToneTexture.magFilter = THREE.NearestFilter;
    
    // minFilter属性使用了NearestFilter, 可以按需为该纹理停用mipmapping,使得GPU不再处理其mip映射
    fiveToneTexture.generateMipmaps = false; 
    
    material.gradientMap = fiveToneTexture;
  }
  material.needsUpdate = true;
});
gui.add(directionalLight.position, 'x', -10, 10, 0.1);
gui.add(directionalLight.position, 'y', -10, 10, 0.1);
gui.add(directionalLight.position, 'z', -10, 10, 0.1);

image.png

法线材质 MeshNormalMaterial

根据物体表面的法向量计算颜色

法线垂直于物体表面

没有 color(颜色)、map(贴图)等属性,对光照没有反应

法向量的作用:决定光的发射方向、在计算光照、阴影时提供信息、为物体表面上色。

法向量所指的方向,决定每个面从 MeshNormalMaterial材质获取到的颜色。

flatShading 和分段数会影响法向量计算结果。

    const meshFolder = gui.addFolder('物体')
    meshFolder.add(_this.material, 'wireframe')

    // flatShading 对曲面起作用,把曲面变成多个平面
    meshFolder.add(_this.material, 'flatShading').onChange(val => {
      // 更新
      _this.material.needsUpdate = true
    })

    // 所有的几何体一旦实例化后,就不能被修改
    meshFolder.add(_this.sphere.geometry.parameters, 'heightSegments', 16, 100, 1).onChange(val => {
      // 像更新纹理一样,先销毁,在重新赋值
      _this.sphere.geometry.dispose()

      const geometry = new THREE.SphereGeometry(0.5, 16, val)
      _this.sphere.geometry = geometry
    })
    

标准材质 MeshStandardMaterial

基于物理(PBR)的标准材质

该材质提供比 MeshLambertMaterial或 MeshPhongMaterial更精确逼真的效果,代价是计算成本更高。

计算着色的方式与MeshPhongMaterial相同,都使用Phong着色模型。

为获得最佳效果,在使用此材质时,应始终指定 environmantMap。

envMap 全景纹理,可模拟对四周环境的镜面反射。

物理材质 MeshPhysicalMaterial

基于物理(PBR)的标准材质。

高级光线反射:为非金属材质,提供更多更灵活的光线反射。

对 MeshStandardMaterial的扩展,提供了更高级的、基于物理(PBR)的渲染属性,能更高的控制反射率

clearcoat类似于车漆、碳纤,具有反光特性的面。

基于物理(PBR)的透光性,transmission属性(感光的厚薄程度)让某些很薄的透明表面,如 玻璃,变得更真实。范围从0.0到1.0,默认值是0.0,越小透光越强。

ior 属性是 非金属材质所设置的反射率(数值越大,类似放大镜效果),范围由1.0~2.333,默认值是1.5

thickness曲面体积下的厚度,若值为0,则材质为薄壁,默认值是0 image.png image.png

scene.background = new THREE.Color(0xffffff) // 场景背景色

  // 创建立方体对象
  createObjects () {
    const colorTexture = this.textureLoader.load('/src/assets/textures/1.jpg')

    const material = new THREE.MeshStandardMaterial({
      map: colorTexture,
    })

    const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), material) // 球体
    const mesh = new THREE.Mesh(
      new THREE.SphereGeometry(1, 64, 64),
      new THREE.MeshPhysicalMaterial({
        // 可结合环境光,呈现玻璃球中透出纹理图的效果
        // envMap:,
        // envMapIntensity:
        roughnessMap: colorTexture,
        roughness: 0.1,
        clearcoat: 1.0, // 具有反光特性
        transmission: 0.8, // 厚度
        ior: 1.0, // 非金属材质的反射率
        thickness: 1.0, // 曲面下体积的厚度
      })
    )

    sphere.position.x = -2
    mesh.position.x = 2

    this.scene.add(sphere, mesh)
    this.mesh = mesh
    this.material = material
  },

// 物体外透明厚度
gui.add(_this.mesh.material, 'clearcoat', 0, 1, 0.1)
// 感光厚度 越小透光越强
gui.add(_this.mesh.material, 'transmission', 0, 1, 0.1)
gui.add(_this.mesh.material, 'ior', 1.0, 2.333, 0.01)
gui.add(_this.mesh.material, 'thickness', 0, 1, 0.01)

360度全景贴图

RGBELoader对象(hdr加载器)缺点:纯粹的环境全景。若想要交互(室内装修,室内物体交互,点击、选中),可使用cubeTexture立方纹理贴图(适用于复杂交互场景)

    // hdr 加载器
    const hdrLoader = new RGBELoader()
    const hdrTexture = hdrLoader.load('1.jpg', 
      // onLoad回调
      ()=> {
        // 场景添加背景图
        this.scene.background = hdrTexture
        // hdrTexture.mapping = THREE.EquirectangularReflectionMapping // 反射映射
        hdrTexture.mapping = THREE.EquirectangularReflectionMapping // 折射映射

        // 给所有物体设置环境贴图 仅支持环境贴图的材质,反射周围的环境  
        this.scene.environment = hdrTexture 
    })

    const sceneFolder = gui.addFolder('场景')
    sceneFolder.add(_this.scene, 'backgroundBlurriness', 0, 1, 0.1) // 背景模糊
    sceneFolder.add(_this.scene, 'backgroundIntensity', 0, 1, 0.1) // 背景强度

    // 调整整个场景曝光度,toneMapping属性 是色调的算法,如手机p图时,调整的全局色调,默认值是不生效
    gui.add({ exposure: 1 }, 'exposure', 0, 10, 0.1).onChange(val =>{
      _this.renderer.toneMapping = THREE.LinearToneMapping // 曝光度线性变化
      _this.renderer.toneMappingExposure = val;
    })
    gui.add(_this.renderer, 'toneMapping', [
      'NoToneMapping',
      'LinearToneMapping',
      'ReinhardToneMapping',
      'CineonToneMapping',
      'ACESFilmicToneMapping',
    ]).onChange(val => {
      _this.renderer.toneMapping = THREE[val]
    })

    //  webglrenderer对象中,outputEncoding属性:定义渲染器的输出编码
    gui.add(_this.renderer, 'outputEncoding', [
      'LinearEncoding',
      'sRGBEncoding',
      'BasicDepthPacking',
      'RGBADepthPacking',
    ]).onChange(val => {
      _this.renderer.outputEncoding = THREE[val]
    })

    // 环境贴图除了用 hdr实现,还可用cubeTexture立方纹理贴图实现,
    gui.add(_this.mesh.material, 'envMap', ['hdr', 'cube']).onChange(val =>{
      if (val === 'cube') {
        _this.scene.background = _this.envTexture
        _this.sphere.material.envMap = _this.envTexture
        _this.box.material.envMap = _this.envTexture
      } else {
        _this.scene.background = _this.hdrTexture
        _this.sphere.material.envMap = _this.hdrTexture
        _this.box.material.envMap = _this.hdrTexture
      }
      _this.sphere.material.needsUpdate = true
      _this.box.material.needsUpdate = true
    })

webglrenderer对象中,outputEncoding属性:定义渲染器的输出编码。

以下0.151.3版本源码内容,问题出现:仅支持LinearEncodingsRGBEncoding两个属性配置。

function getEncodingComponents( encoding ) {
    switch ( encoding ) {
        case LinearEncoding:
            return [ 'Linear', '( value )' ];
        case sRGBEncoding:
            return [ 'sRGB', '( value )' ];
        default:
            console.warn( 'THREE.WebGLProgram: Unsupported encoding:', encoding );
            return [ 'Linear', '( value )' ];
    }
}

阴影材质 ShadowMaterial

  • 生成阴影机制:
  1. 物体castShadow属性产生阴影
  2. 平面receiveShadow属性接收阴影(默认不开启)
  3. 让平行光照castShadow属性产生阴影
  4. render中开启shadowMap属性
  • 阴影材质特性:只有在有阴影的地方,才不是透明的,其他全是透明的
import { useEffect } from 'react';
import * as THREE from 'three';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import * as dat from 'dat.gui';

const Page = () => {
  useEffect(() => {
    const $ = {
      createScene () {
        const canvas = document.getElementById('c')

        const width = window.innerWidth;
        const height = window.innerHeight;

        canvas.width = width
        canvas.height = height
        // 挂载全局上
        this.canvas = canvas
        this.width = width
        this.height = height

        // 创建3D场景对象
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0xf0f0f0)
        this.scene = scene
      },
      // 创建光照
      createLights () {
        // 添加全局光照
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7)

        // 改变光照方向
        directionalLight.position.set(1, 2, 2)
        directionalLight.castShadow = true 
        directionalLight.shadow.camera.near = 0.1
        directionalLight.shadow.camera.far = 40
        directionalLight.shadow.radius = 1.5 // 越小越清晰
        directionalLight.shadow.mapSize.x = 1024; // mapSize 影响阴影模糊,越大越清晰
        directionalLight.shadow.mapSize.y = 1024

        this.scene.add(ambientLight, directionalLight);
        this.directionalLight = directionalLight
      },
      // 纹理
      loadTextures () {
        // -----------------方法一
        // const img = new Image()
        // // 创建纹理
        // const texture = new THREE.Texture(img)

        // img.onload = function () {
        //   console.log(texture);
        //   // 更新纹理
        //   texture.needsUpdate = true
        // }
        // img.src = '/src/assets/textures/Wood_Ceiling_Coffers_003_basecolor.Cu38ry6v.jpg';
        // this.texture = texture;

        // ------------------方法二
        // setCrossOrigin('anonymous') 跨域方法
        // const textLoader = new THREE.TextureLoader()
        // this.texture =  textLoader.setCrossOrigin('anonymous').load(
        //   // this.texture =  textLoader.load(
        //   // 'https://3dbooks.netlify.app/assets/Wood_Ceiling_Coffers_003_basecolor.Cu38ry6v.jpg',
        //   '/src/assets/textures/Wood_Ceiling_Coffers_003_basecolor.Cu38ry6v.jpg',
        //   // onLoad回调
        //   function (texture) {},
        //   null,
        //   // onError回调
        //   (error) => {
        //     console.log('error', error);
        //   }
        // )

        // --------------------方法三
        const manager = new THREE.LoadingManager()
        manager.onStart = function( url, itemsLoaded, itemsTotal) {
          console.log( 'Start loading file: '+url +'.\nLoaded' + itemsLoaded+'of '+ itemsTotal+'files.');
        }

        manager.onLoad = function() {
          console.log('Loading complete !');
        }

        manager.onProgress = function( url, itemsLoaded, itemsTotal ){
          console.log('Loading file: '+ url+ '.\Loaded ' +itemsLoaded+ ' of '+itemsTotal+' files. ');
        }

        manager.onError = (url) =>{
          console.log('There was an error loading '+ url);
        }
        
        const textureLoader = new THREE.TextureLoader(manager)
        this.textureLoader = textureLoader
      },
      // 创建立方体对象
      createObjects () {
        // 阴影材质特性:只有在有阴影的地方,才不是透明的,其他全是透明的
        // 生成阴影机制: 1. 物体castShadow属性产生阴影 2. 平面receiveShadow属性接收阴影(默认不开启) 3. 让平行光照castShadow属性产生阴影 4. render中开启shadowMap属性
        const floorTexture = this.textureLoader.load('/src/assets/textures/6.jpg')
        const wallTexture = this.textureLoader.load('/src/assets/textures/2.jpg')

        this.floorTexture = floorTexture
        this.wallTexture = wallTexture

        // 在展示阴影中,两个平面会重叠
        const material = new THREE.ShadowMaterial({
          opacity: 1,
          polygonOffset: true, // 开启多边形偏移
          polygonOffsetFactor: -1,// 多边形偏移系数,默认值是0
        }) 

        const box = new THREE.Mesh(
          new THREE.BoxGeometry(1, 1, 1),
          new THREE.MeshBasicMaterial({
            color: 0x1890ff,
          })
        )

        // 平面阴影
        const planeShadow = new THREE.Mesh(
          new THREE.PlaneGeometry(10, 10),
          material
        )
        // 地板
        const floor = new THREE.Mesh(
          new THREE.PlaneGeometry(10, 10),
          new THREE.MeshBasicMaterial({
            map: floorTexture,
          })
        )

        // 墙面阴影
        const wallShadow = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), material)
        // 墙面
        const wall = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), new THREE.MeshBasicMaterial({
          map: wallTexture,
        }) )

        planeShadow.rotation.x = -Math.PI / 2
        planeShadow.position.y = -0.8
        floor.rotation.x = -Math.PI / 2
        floor.position.y = -0.8
        floor.position.z = -2
        wallShadow.position.y = 4
        wallShadow.position.z = -2
        wall.position.z = -2
        box.castShadow = true // 产生阴影
        wallShadow.receiveShadow = true // 墙面接收阴影
        planeShadow.receiveShadow = true // 平面接收阴影

        this.scene.add(box, planeShadow, floor, wallShadow, wall)
      },
      createCamera () {
        const size = 4;
        // 创建正交相机
        const orthoCamera = new THREE.OrthographicCamera(-size, size, size / 2, -size / 2, 0.1, 10);
        // 相机位置
        orthoCamera.position.set(0, 1, 3)
        // 设置相机朝向
        orthoCamera.lookAt(this.scene.position)
        // 相机添加场景中
        this.scene.add(orthoCamera)
        this.orthoCamera = orthoCamera
        // this.camera = orthoCamera
        // console.log(orthoCamera);

        // 透视相机 第二个相机
        const watcherCamera = new THREE.PerspectiveCamera(75, this.width / this.height, 0.1, 100)
        // 设置相机位置
        watcherCamera.position.set(0, 2, 4)
        // 设置相机朝向
        watcherCamera.lookAt(this.scene.position)
        // 将相机添加到场景中
        this.scene.add(watcherCamera)
        this.watcherCamera = watcherCamera
        this.camera = watcherCamera
      },
      datGui () {
        const _this = this
        const gui = new dat.GUI();

        gui.add(_this.directionalLight.position, 'x', -10, 10, 0.1)
        gui.add(_this.directionalLight.position, 'y', -10, 10, 0.1)
        gui.add(_this.directionalLight.position, 'z', -10, 10, 0.1)
      },
      // 添加辅助
      helpers () {
        // 创建辅助坐标系
        const axesHelper = new THREE.AxesHelper();

        const gridHelper = new THREE.GridHelper(20, 20, 0xf0f0f0)
        gridHelper.position.y = -1
        
        this.scene.add(axesHelper, gridHelper)
      },
      render () {
        // 创建渲染器
        const renderer = new THREE.WebGLRenderer({
          canvas: this.canvas,
          antialias: true
        })
        renderer.shadowMap.enabled = true;
        // 设置渲染器屏幕像素比 移动端解决像素问题
        renderer.setPixelRatio(window.devicePixelRatio || 1)
        // 设置渲染器大小
        renderer.setSize(this.width, this.height)
        // 执行渲染
        renderer.render(this.scene, this.camera)
        this.renderer = renderer
      },
      controls () {
        // 创建轨道控制器
        const orbitControls = new OrbitControls(this.camera, this.canvas)
        // 开启惯性
        orbitControls.enableDamping = true;
        this.orbitControls = orbitControls
      },
      tick () {
        // 更新
        this.orbitControls.update()

        this.renderer.render(this.scene, this.camera)
        window.requestAnimationFrame(()=> this.tick())
      },
      fitView () {
        // 监听窗口大小变化
        window.addEventListener('resize', () =>{
          this.camera.aspect = window.innerWidth / window.innerHeight;
          this.camera.updateProjectionMatrix();
          this.renderer.setSize(window.innerWidth, window.innerHeight)
        }, false)
      },
      init () {
        this.createScene()
        this.createLights()
        this.loadTextures()
        this.createObjects()
        this.createCamera()
        this.helpers()
        this.render()
        this.controls()
        this.tick()
        this.fitView()
        this.datGui()
      }
    }

    $.init();
  }, []);
  return <>
    <canvas id="c" />;
  </>
};
export default Page;

总结

  • 标准材质 + 物理材质 材质高级,但更加消耗性能;