threejs 镜面反射 地板具备镜面反射的效果,并且具备低透明度。

前言

效果如下:

图片

本次文章要分享的内容有以下几点:

  • 镜面反射

  • 文字渲染

1. 镜面反射

本次开发的3D场景,地板具备镜面反射的效果,并且具备低透明度。

1.1 Reflector

threejs官方实例中提供了名为mirrorReflector应用实例。

对场景中的一面墙和地板进行了镜面反射。

图片

其使用方式也极为简单,只需要将geometry传入其中并设置参数即可:

 
  1. let geometry, material; 

  2. geometry = new THREE.PlaneGeometry( 100, 100 ); 

  3. groundMirror = new Reflector( geometry, { 

  4.     clipBias: 0, 

  5.     textureWidth: window.innerWidth * window.devicePixelRatio, 

  6.     textureHeight: window.innerHeight * window.devicePixelRatio, 

  7.     color: 0x777777, 

  8.     opacity: 0.5, 

  9. }); 

  10. groundMirror.position.y = 10; 

  11. groundMirror.rotateX( - Math.PI / 2 ); 

  12. scene.add( groundMirror );

1.2 Reflector的缺陷 - 半透明

将Reflector直接拿来使用是不能满足需求的,因为官方提供的Reflector不具备半透明的能力。

在实际使用时会像一面完全光滑的镜子来进行反射。

那么想要实现半透明能力,在理论中有一下几种方法:

  1. 地板叠在镜面之上,地板半透明

  2. 镜面叠在地板之上,镜面半透明

  3. 地板这一物体本身可以进行镜面反射

在最开始项目较急时选择了第1种方案,只需要地板降低透明度就可以达到效果。

在后来阅读了Reflector的源码后选择进行扩展。

1.3 扩展Reflector - 实现镜面半透明

这里选择对Reflector进行扩展,以支撑我们的需求,使镜面实现半透明。

这里对Reflector Shader进行简单修改。

旧 Reflector Shader
Reflector.ReflectorShader = {
  uniforms: {
    'color': {
      value: null
    },
    'tDiffuse': {
      value: null
    },
    'textureMatrix': {
      value: null
    }
  },
  vertexShader: /* glsl */`
    uniform mat4 textureMatrix;
    varying vec4 vUv;
    void main() {
      vUv = textureMatrix * vec4( position, 1.0 );
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,
  fragmentShader: /* glsl */`
    uniform vec3 color;
    uniform sampler2D tDiffuse;
    varying vec4 vUv;
    float blendOverlay( float base, float blend ) {
      return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
    }
    vec3 blendOverlay( vec3 base, vec3 blend ) {
      return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
    }
    void main() {
      vec4 base = texture2DProj( tDiffuse, vUv );
      gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 );
    }`
};
新 Reflector Shader
Reflector.ReflectorShader = {
  uniforms: {
    color: {
      value: null,
    },
    tDiffuse: {
      value: null,
    },
    textureMatrix: {
      value: null,
    },
    // 新增uniforms.opacity
    opacity: {
      value: 1,
    },
  },
  vertexShader: /* glsl */ `
    uniform mat4 textureMatrix;
    varying vec4 vUv;
    void main() {
      vUv = textureMatrix * vec4( position, 1.0 );
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,
  fragmentShader: /* glsl */ `
    uniform vec3 color;
    uniform sampler2D tDiffuse;
    uniform float opacity; // 引入opacity
    varying vec4 vUv;
    float blendOverlay( float base, float blend ) {
      return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
    }
    vec3 blendOverlay( vec3 base, vec3 blend ) {
      return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
    }
    void main() {
      vec4 base = texture2DProj( tDiffuse, vUv );
      // 使用opacity
      gl_FragColor = vec4( blendOverlay( base.rgb, color ), opacity );
    }`,
};

此处在原有基础上扩展了opacity参数。

整个镜面物体的透明度将会随着opacity变化。

图片

1.4 改造Reflector - 实现地板反射

第三种方案则是将地板材质与反射进行混合,将二者融为一体,而不是层叠。并通过参数控制反射强度。

首先找一张地板贴图:

图片

改造 Reflector Shader

引入贴图,使用mix进行混合。

Reflector.ReflectorShader = {
  uniforms: {
    'color': {
      value: null
    },
    'tDiffuse': {
      value: null
    },
    'textureMatrix': {
      value: null
    },
    'opacity': {
      value: 1
    },
    'floorTexture': {
      value: null
    },
  },
  vertexShader: /* glsl */`
    uniform mat4 textureMatrix;
    varying vec4 vUv;
    varying vec2 vUv2;
    void main() {
      vUv2 = uv;
      vUv = textureMatrix * vec4( position, 1.0 );
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,
  fragmentShader: /* glsl */`
    uniform vec3 color;
    uniform sampler2D tDiffuse;
    uniform sampler2D floorTexture;
    uniform float opacity;
    varying vec4 vUv;
    varying vec2 vUv2;
    float blendOverlay( float base, float blend ) {
      return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
    }
    vec3 blendOverlay( vec3 base, vec3 blend ) {
      return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
    }
    void main() {
      vec4 floor = texture2D( floorTexture, vUv2 );
      vec4 base = texture2DProj( tDiffuse, vUv );
      gl_FragColor = vec4( mix(floor.rgb, blendOverlay( base.rgb, color ), opacity), 1.0 );
    }`
};

opacity 参数从 0 - 1 控制镜面反射强度。

效果:

图片

图片

成功实现了某平面的镜面反射。

1.5 其他

扩展Reflector的目的主要是为了实现需求。

分享中只是使用mix函数进行简单的混合。

各位同学在实际开发时可以根据业务需要,针对材质使用对应的混合算法来达到更加拟真的效果。

2. 文字

在日常开发中会有很多的文字加到场景里。

通常使用THREE.Font动态生成文字材质两种文字加载方式。

使用方式有:

  1. THREE.TextGeometry

  2. THREE.ShapeGeometry

  3. THREE.SpriteMaterial

  4. THREE.PlaneGeometry

前两种对应THREE.Font,后两种对应动态生成文字材质

本次将会分享几种方法的特点和使用方法。

2.1 THREE.Font

Font对象通常是由THREE.FontLoader加载生成。

生成后传入到THREE.TextGeometry中生成立体文字。

图片

或使用Font.generateShapes生成Shapes数组传入到THREE.ShapeGeometry使用。

图片

FontLoader.load则需传入JSON格式的typeface

官方在文档中推荐使用facetype.js来进行转换。

那么在各位同学开发时,遇到没有设计团队或设计师的情况下,无法提供出字体文件时需要自行寻找字体资源。

要注意,字体风格的圆角越多,则字体文件越大。

个别毛笔字字体文件大小高达50m。

本篇文章使用免费的中文字体:字制区喜脉体来为大家做实例。

难点在于:需要在项目中使用typeface.js来进行字体文件 => THREE.Font 的转换

typeface.js

将typeface的主要代码进行拷贝,并改造

import OpenType from './opentype.min';

const convert = (fontStream, options = {}) => {
  // 改变参数,原本是 .checked
  const {
    filetypeJson = true,
    restrictCharactersCheck = false,
    reverseTypeface = false,
    restrictCharacterSetInput = '',
  } = options;

  // 手动使用opentype进行转换
  const font = OpenType.parse(fontStream);

  const scale = (1000 * 100) / ((font.unitsPerEm || 2048) * 72);
  const result = {};
  result.glyphs = {};

  const restriction = {
    range: null,
    set: null,
  };

  if (restrictCharactersCheck) {
    const restrictContent = restrictCharacterSetInput;
    const rangeSeparator = '-';
    if (restrictContent.indexOf(rangeSeparator) !== -1) {
      const rangeParts = restrictContent.split(rangeSeparator);
      if (
        rangeParts.length === 2
        && !Number.isNaN(rangeParts[0])
        && !Number.isNaN(rangeParts[1])
      ) {
        restriction.range = [parseInt(rangeParts[0], 10), parseInt(rangeParts[1], 10)];
      }
    }
    if (restriction.range === null) {
      restriction.set = restrictContent;
    }
  }

  font.glyphs.forEach((glyph) => {
    if (glyph.unicode !== undefined) {
      const glyphCharacter = String.fromCharCode(glyph.unicode);
      let needToExport = true;
      if (restriction.range !== null) {
        needToExport = glyph.unicode >= restriction.range[0]
          && glyph.unicode <= restriction.range[1];
      } else if (restriction.set !== null) {
        needToExport = restrictCharacterSetInput.indexOf(glyphCharacter) !== -1;
      }
      if (needToExport) {
        const token = {};
        token.ha = Math.round(glyph.advanceWidth * scale);
        token.x_min = Math.round(glyph.xMin * scale);
        token.x_max = Math.round(glyph.xMax * scale);
        token.o = '';
        if (reverseTypeface) {
          glyph.path.commands = reverseCommands(glyph.path.commands);
        }
        glyph.path.commands.forEach((command) => {
          if (command.type.toLowerCase() === 'c') {
            command.type = 'b';
          }
          token.o += command.type.toLowerCase();
          token.o += ' ';
          if (command.x !== undefined && command.y !== undefined) {
            token.o += Math.round(command.x * scale);
            token.o += ' ';
            token.o += Math.round(command.y * scale);
            token.o += ' ';
          }
          if (command.x1 !== undefined && command.y1 !== undefined) {
            token.o += Math.round(command.x1 * scale);
            token.o += ' ';
            token.o += Math.round(command.y1 * scale);
            token.o += ' ';
          }
          if (command.x2 !== undefined && command.y2 !== undefined) {
            token.o += Math.round(command.x2 * scale);
            token.o += ' ';
            token.o += Math.round(command.y2 * scale);
            token.o += ' ';
          }
        });
        result.glyphs[String.fromCharCode(glyph.unicode)] = token;
      }
    }
  });
  result.familyName = font.familyName;
  result.ascender = Math.round(font.ascender * scale);
  result.descender = Math.round(font.descender * scale);
  result.underlinePosition = Math.round(
    font.tables.post.underlinePosition * scale,
  );
  result.underlineThickness = Math.round(
    font.tables.post.underlineThickness * scale,
  );
  result.boundingBox = {
    yMin: Math.round(font.tables.head.yMin * scale),
    xMin: Math.round(font.tables.head.xMin * scale),
    yMax: Math.round(font.tables.head.yMax * scale),
    xMax: Math.round(font.tables.head.xMax * scale),
  };
  result.resolution = 1000;
  result.original_font_information = font.tables.name;
  if (font.styleName.toLowerCase().indexOf('bold') > -1) {
    result.cssFontWeight = 'bold';
  } else {
    result.cssFontWeight = 'normal';
  }

  if (font.styleName.toLowerCase().indexOf('italic') > -1) {
    result.cssFontStyle = 'italic';
  } else {
    result.cssFontStyle = 'normal';
  }

  if (filetypeJson) {
    return JSON.stringify(result);
  }
  return `if (_typeface_js && _typeface_js.loadFace) _typeface_js.loadFace(${JSON.stringify(
    result,
  )});`;
};

export default convert;
CustomFontLoader

包装复杂的Loader操作。

import * as THREE from 'three';
import Typeface from './typeface';           // 引入typeface

export default function FontLoader(fontUrl) {
  const fileLoader = new THREE.FileLoader(); // 请求字体文件

  return new Promise((resolve, reject) => {
    fileLoader.setResponseType('blob');
    fileLoader.setMimeType('font');
    fileLoader.load(
      fontUrl,
      (data) => {
        const reader = new FileReader();     // 将请求的Blob内容转换为ArrayBuffer
        reader.addEventListener(
          'load',
          (event) => {
            const font = new THREE.Font(
              // 生成font对象
              JSON.parse(Typeface(event.target.result)),
            );
            resolve(font);
          },
          false,
        );
        reader.readAsArrayBuffer(data);
      },
      () => {},
      reject,
    );
  });
}
使用
FontLoader('/static/ChineseText/字制区喜脉体.ttf').then((font) => {
  const textGeo = new THREE.TextGeometry(text, {
    font,

    size: 40,
    height: 20,
    curveSegments: 4,
    bevelThickness: 2,
    bevelSize: 1.5,
    bevelEnabled: true,
  });
  textGeo.computeBoundingBox();
  const centerOffset = -0.5 * (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x);
  const materials = [
    new THREE.MeshPhongMaterial({ color: 0xff00ff, flatShading: true }), // front
    new THREE.MeshPhongMaterial({ color: 0xffffff }), // side
  ];
  const textMesh1 = new THREE.Mesh(textGeo, materials);
  textMesh1.position.x = centerOffset;
  textMesh1.position.y = 30;
  textMesh1.position.z = 0;
  textMesh1.rotation.x = 0;
  textMesh1.rotation.y = Math.PI * 2;

  scene.add(textMesh1);
});

效果:

图片

2.2 动态生成文字材质

相对于THREE.Font的繁琐,动态生成文字材质更加方便易理解。

  1. 创建canvas

  2. 在canvas上写字

  3. 将canvas变为材质

  4. 贴给某物体

相较于Font,动态生成的文字不够精细,因为材质是位图。而Font的本质则是矢量图。

想要保持清晰则需要放大生成的canvas和写在其上的文字。

css注册文字
@font-face {
  font-family: 'XM-Font';
  src: url('/static/ChineseText/字制区喜脉体.ttf');
}
生成材质
function getTextWidth(text, font) {
  const span = document.createElement("span");
  span.innerText = text;
  span.style.font = font;
  document.body.appendChild(span);
  const rect = span.getBoundingClientRect();
  const width = rect.right - rect.left;
  span.remove();
  return width;
}

function createText(text, fontSize = 40, fontName = 'sans-serif') {
  // 先用画布将文字画出
  const font = `lighter ${fontSize}px ${fontName}`;
  const canvas = document.createElement("canvas");
  document.body.appendChild(canvas);
  canvas.width = getTextWidth(text, font);
  canvas.height = fontSize + 4;

  const ctx = canvas.getContext("2d");

  ctx.fillStyle = "#000";
  ctx.font = font;
  ctx.lineWidth = 4;
  ctx.fillText(text, 0, fontSize);
  const texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;

  const material = new THREE.MeshBasicMaterial({ // 此处可改为SpriteMaterial
    map: texture,
    alphaTest: 0.1,  // 降低文字模糊的几率
    transparent: true,
  });
  const geometry = new THREE.PlaneGeometry(10, 10, 1);

  const mesh = new THREE.Mesh(geometry, material);
  mesh.scale.set(canvas.width / 4, fontSize / 4, fontSize / 2);

  canvas.remove();
  return mesh;
}
使用
scene.add(createText('测试文字', 40, 'XM-Font'))

效果:

图片

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
three.js是一个用于创建3D图形和动画的JavaScript库。在three.js中,镜面反射是一种能够模拟物体表面如镜子一样反射环境和其他物体的效果。 要实现镜面反射,首先需要定义一个具有镜面材质的对象。镜面材质能够根据环境和其他物体的位置动态地反射光线。创建镜面材质的方法如下: ```javascript var mirrorMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, // 镜面颜色 specular: 0x999999, // 镜面高亮颜色 envMap: reflectionCube, // 环境贴图 reflectivity: 0.8 // 反射强度 }); ``` 在镜面材质中,我们可以通过设置颜色、高亮颜色、环境贴图和反射强度来控制镜面反射效果。 接下来,我们需要为对象创建一个可渲染的几何体,并将镜面材质应用于该几何体: ```javascript var mirrorGeometry = new THREE.PlaneGeometry(10, 10); // 创建一个平面几何体 var mirrorMesh = new THREE.Mesh(mirrorGeometry, mirrorMaterial); // 创建一个带有镜面材质的网格 ``` 最后,将该对象添加到场景中进行渲染: ```javascript scene.add(mirrorMesh); ``` 此时,镜面材质中定义的对象将以镜子一样的方式反射场景中的光线和其他物体,从而实现镜面反射效果。 需要注意的是,为了使镜面反射更真实,我们还可以通过环境贴图来模拟反射物体的环境。环境贴图通常是使用三次元图像来定义反射环境,使得镜面反射更加真实,并能够反射出周围物体的样子。 综上所述,通过在three.js中使用镜面材质和环境贴图,可以实现真实的镜面反射效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值