ThreeJS-3D教学十四:ShaderMaterial(length、fract、step)


length() 内置函数可以获取向量的长度,

这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,

会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,

对应颜色正好是白色,当dist>1后,颜色仍为白色。

颜色突变 GLSL函数 step 、fract

除了渐变,我们可以结合 GLSL 的内置函数做出颜色突变的效果,借助 step(edge, x) 函数,其会返回0.0或1.0数值,如果 x<edge 返回0.0,如果 x>edge 返回1.0。

step(0.5, vUv.x) 通过 vUv.x 和 0.5(vUv中心点 0.5 0.5) 比较,小于0.5的返回0.0,大于0.5的返回1.0,并将该 color 变成转换成 vec3() 格式,于是就是黑白突变的格式。

float color = step(0.5, vUv.x);

gl_FragColor = vec4(vec3(color), 1.0);

fract() 函数取小数使得数值在 0.0-1.0 里循环重复,

比如1.1、2.1取小数后都变回0.1,再将该数值转换成 vec3 再设置到颜色上,

就会产生重复的黑白渐变效果。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    body {
      width: 100%;
      height: 100%;
    }

    * {
      margin: 0;
      padding: 0;
    }

    .label {
      font-size: 20px;
      color: #fff;
      font-weight: 700;
    }
  </style>
</head>

<body>
<div id="container"></div>
<script type="importmap">
  {
    "imports": {
      "three": "../three-155/build/three.module.js",
      "three/addons/": "../three-155/examples/jsm/"
    }
  }
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
let stats, labelRenderer, gpuPanel, shaderMaterial, boxShaderMaterial;
let camera, scene, renderer, mesh, target, controls, shaderMaterialSphere;
const group = new THREE.Group();
let time = 0;
let time1 = 0;
init();
initHelp();
initLight();
axesHelperWord();
animate();

const box = boxModel();
box.position.set(0, 120, 0);
scene.add(box);

let fragmentShader11 = [
  "varying vec2 vUv;",
  "uniform float uTime;",
  "void main(){",
  " gl_FragColor = vec4(vUv, 1.0, 1.0);",
  "}"
].join('\n');
const sphere11 = sphereModel1(fragmentShader11);
sphere11.position.set(250, 120, -250);
scene.add(sphere11);

let fragmentShader12 = [
  "varying vec2 vUv;",
  "uniform float uTime;",
  "void main(){",
  " float color = step(0.5, vUv.y);",
  " gl_FragColor = vec4(vec3(color), 1.0);",
  "}"
].join('\n');
const sphere12 = sphereModel1(fragmentShader12);
sphere12.position.set(250, 120, -100);
scene.add(sphere12);

let fragmentShader13 = [
  "varying vec2 vUv;",
  "uniform float uTime;",
  "void main(){",
  " float color = step(uTime, fract(vUv.y * 5.0));",
  " gl_FragColor = vec4(vec3(color), 1.0);",
  "}"
].join('\n');
const sphere13 = sphereModel1(fragmentShader13);
sphere13.position.set(250, 120, 50);
scene.add(sphere13);

function sphereModel1(fragmentShader) {
  let vertexShader = [
    "varying vec2 vUv;",
    "void main(){",
    "	gl_Position	= projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
    " vUv = uv;",
    "}"
  ].join('\n');

  /**
   * 颜色渐变
   * 就是照理 position 是每个顶点的坐标,uv 是每个顶点的纹理坐标
   * shader 里的代码是对每个顶点或片元单独执行的,这里的 vUv 就只是每个片元其各自的数值
   * (每个片元甚至连周围片元的数值是多少都不知道),
   * 比如左下角 vUv 为 (0.0, 0.0) 所以对应的 vUv.x=vUv.y=0.0,颜色为 vec4(0.0, 0.0, 0.0, 1.0)
   * 即黑色,同理把左上角 (0.0, 1.0)、右下角 (1.0, 0.0)、右上角 (1.0, 1.0)、最中间 (0.5, 0.5)
   * 等每个位置的数值分别带入上面的代码,就能得到上图的效果。
   */

  // gl_FragColor.a = 0.9;
  // gl_FragColor.rgb = vec3(1, 1, 1)

  // 如果用 vUv 里的 x 或 y 分量分别设置到 rgba 颜色里的 red 通道并赋值给
  // gl_FragColor,会有左右或者上下的黑色到红色的渐变效果。
  // gl_FragColor = vec4(vUv.x, 0.0, 0.0, 1.0);
  // gl_FragColor = vec4(vUv.y, 0.0, 0.0, 1.0);
  // gl_FragColor = vec4(vec3(vUv.x), 1.0);
  // gl_FragColor = vec4(vec3(vUv.y), 1.0);

  // 如果将 vUv 设置到 red 和 green 通道、blue 通道设为0.0,
  // 就是这个非常常见的 uv 青色红色颜色效果,如果大家用过其他一些3D软件,应该对这个图并不陌生。
  // gl_FragColor = vec4(vUv, 0.0, 1.0);

  /**
    * 颜色突变 GLSL函数 step 、fract
      除了渐变,我们可以结合 GLSL 的内置函数做出颜色突变的效果,借助 step(edge, x) 函数,
      其会返回0.0或1.0数值,如果 x<edge 返回0.0,如果 x>edge 返回1.0。step(0.5, vUv.x)
      通过 vUv.x 和 0.5(vUv中心点 0.5 0.5) 比较,小于0.5的返回0.0,大于0.5的返回1.0,并将该 color 变成转换成 vec3() 格式,
      于是就是黑白突变的格式。
      float color = step(0.5, vUv.x);
      gl_FragColor = vec4(vec3(color), 1.0);
    */

  /**
   * fract() 函数取小数使得数值在 0.0-1.0 里循环重复,
   * 比如1.1、2.1取小数后都变回0.1,再将该数值转换成 vec3 再设置到颜色上,
   * 就会产生重复的黑白渐变效果。
   */
  

  const planeGeo = new THREE.SphereGeometry(45, 30, 30);
  shaderMaterialSphere = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    side: THREE.DoubleSide,
    uniforms: {
      uTime: { value: 0.5 },
    },
    transparent: false,
    depthWrite: true
  });
  return new THREE.Mesh(planeGeo, shaderMaterialSphere);
}

function boxModel() {
  let vertexShader = [
    "varying vec2 vUv;",
    "void main(){",
    "	gl_Position	= projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
    " vUv = uv;",
    "}"
  ].join('\n');


  // 当我们有了每个 uv 离中心的距离后,可以对它运用翻倍再取小数的操作进行重复,
  // 这样就能做出由里向外一圈圈黑白交错的径向条纹效果。

  // 有个小细节需要注意,上面我们翻了5倍,但看效果里一黑一白为1组,其实只有3组多点,
  // 究其原因是 length(vUv - vec2(0.5)) 一开始的范围并不是0到1,最大值是四个角离中心的距离,
  // 也就是 (1.0, 1.0) 离 (0.5, 0.5) 的距离,
  // 即 √2/2=0.707,因而我们可以先对其除以 0.707 再去翻倍取小数进行重复,
  // 这样就能如愿想有几组就几组、想重复几次就重复几次。

  // 然后将 uTime 加到变换到0-1范围后的数值上,使得径向条纹动起来
  // 反向运动可以减去 uTime,运动速率可以通过 uTime 的倍数来控制。

let fragmentShader = `
  varying vec2 vUv;
  uniform float uTime;
  void main() {
    // 先居中,再绘制圆形
    float dist = fract((length(vUv - vec2(0.5)) / 0.707 - uTime * 0.5) * 5.0);
    float radius = 0.5;
    vec3 color = vec3(step(radius, dist));
    gl_FragColor = vec4(color, 1.0);
  }
`;
  const planeGeo = new THREE.BoxGeometry(70, 70, 70);
  boxShaderMaterial = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    side: THREE.DoubleSide,
    uniforms: {
      uTime: { value: 0 },
    },
    transparent: false,
    depthWrite: true
  });
  return new THREE.Mesh(planeGeo, boxShaderMaterial);
}

/**
 * 通过 length() 内置函数可以获取向量的长度,
 * 这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,
 * 会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,
 * 对应颜色正好是白色,当dist>1后,颜色仍为白色。
 */
let fragmentShader1 = `
varying vec2 vUv;
uniform float uTime;
void main() {
  float dist = length(vUv);
  vec3 color = vec3(dist);
  gl_FragColor = vec4(color, 1.0);
}
`;
const sphere1 = sphereModel(fragmentShader1);
sphere1.position.set(-250, 120, -250);
scene.add(sphere1);

// 绘制渐变圆形
let fragmentShader2 = `
varying vec2 vUv;
uniform float uTime;
void main() {
  float dist = length(vUv);
  vec3 color = vec3(step(0.5, dist));
  gl_FragColor = vec4(color, 1.0);  // 1.0 是透明度
}
`;
const sphere2 = sphereModel(fragmentShader2);
sphere2.position.set(-250, 120, -100);
scene.add(sphere2);

let fragmentShader3 = `
varying vec2 vUv;
uniform float uTime;
void main() {
  // float dist = length(vUv - vec2(0.5));
  float dist = distance(vUv, vec2(0.5));
  float radius = 0.5; // 0.25
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}
`;
const sphere3 = sphereModel(fragmentShader3);
sphere3.position.set(-250, 120, 50);
scene.add(sphere3);

let fragmentShader4 = `
varying vec2 vUv;
uniform float uTime;
void main() {
  // 先重复 uv,再居中,再绘制圆形
  float dist = length(fract(vUv * 5.0) - vec2(0.5));
  float radius = 0.5; // 0.25
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}
`;
const sphere4 = sphereModel(fragmentShader4);
sphere4.position.set(-250, 120, 200);
scene.add(sphere4);

let fragmentShader5 = `
varying vec2 vUv;
uniform float uTime;
void main() {
  // 先居中,再绘制圆形
  float dist = length(fract(vUv * 5.0) - vec2(0.5));
  // float dist = distance(vUv, vec2(0.5));
  float radius = 0.5 * (sin(uTime + vUv.x) * 0.5 + 0.5); // 0.25
  vec3 color = vec3(step(radius, dist));
  gl_FragColor = vec4(color, 1.0);
}
`;
const sphere5 = sphereModel(fragmentShader5);
sphere5.position.set(-250, 120, 350);
scene.add(sphere5);

function sphereModel(fragmentShader) {
  let vertexShader = [
    "varying vec2 vUv;",
    "void main(){",
    "	gl_Position	= projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
    " vUv = uv;",
    "}"
  ].join('\n');

  /**
   // 绘制渐变圆形
    float dist = length(vUv);
    vec3 color = vec3(dist);
    gl_FragColor = vec4(color, 1.0);

    // 绘制圆形
    float dist = length(vUv);
    vec3 color = vec3(step(0.5, dist));
    gl_FragColor = vec4(color, 1.0);

    // 先居中,再绘制圆形
    float dist = length(vUv - vec2(0.5));
    // float dist = distance(vUv, vec2(0.5));
    float radius = 0.5; // 0.25
    vec3 color = vec3(step(radius, dist));
    gl_FragColor = vec4(color, 1.0);

    // 先重复 uv,再居中,再绘制圆形
    float dist = length(fract(vUv * 5.0) - vec2(0.5));

    但此时每个圆圈都是通过 sin(uTime) 控制动画,半径变化同步进行,很统一也很单调,
    这里可以通过 sin(uTime + vUv.x) 将不同水平值作为偏差值加进去,
    于是会有水平波浪起伏的效果

    如果再把 vUv.y 也一起加上,变化效果更有趣丰富。
    float radius = 0.5 * (sin(uTime + vUv.x + vUv.y) * 0.5 + 0.5);
    */
  const planeGeo = new THREE.BoxGeometry(70, 70, 70);
  shaderMaterial = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    side: THREE.DoubleSide,
    uniforms: {
      uTime: { value: 0 },
    },
    transparent: false,
    depthWrite: true
  });
  return new THREE.Mesh(planeGeo, shaderMaterial);
}

function init() {

  camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 10, 2000);
  camera.position.set(0, 500, 500);
  camera.up.set(0, 1, 0);
  camera.lookAt(0, 0, 0);

  scene = new THREE.Scene();

  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  labelRenderer = new CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0px';
  labelRenderer.domElement.style.pointerEvents = 'none';
  document.getElementById('container').appendChild(labelRenderer.domElement);

  window.addEventListener('resize', onWindowResize);

  controls = new OrbitControls(camera, renderer.domElement);
  controls.minDistance = 10;
  controls.maxDistance = 1000;
  // 设置为true可启用阻尼(惯性),可用于为控件提供重量感。默认值为false。
  // 请注意,如果启用了此选项,则必须在动画循环中调用.update()。
  controls.enableDamping = false;
  controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动

  stats = new Stats();
  document.body.appendChild(stats.dom);

  gpuPanel = new GPUStatsPanel(renderer.getContext());
  stats.addPanel(gpuPanel);
  stats.showPanel(0);

  scene.add(group);
}

function initLight() {
  const light = new THREE.DirectionalLight(new THREE.Color('rgb(253,253,253)'));
  light.position.set(100, 100, -10);
  light.intensity = 3; // 光线强度
  light.castShadow = true; // 是否有阴影
  light.shadow.mapSize.width = 2048; // 阴影像素
  light.shadow.mapSize.height = 2048;
  // 阴影范围
  const d = 80;
  light.shadow.camera.left = -d;
  light.shadow.camera.right = d;
  light.shadow.camera.top = d;
  light.shadow.camera.bottom = -d;
  light.shadow.bias = -0.0005; // 解决条纹阴影的出现
  // 最大可视距和最小可视距
  light.shadow.camera.near = 0.01;
  light.shadow.camera.far = 2000;
  const AmbientLight = new THREE.AmbientLight(new THREE.Color('rgb(255,255,255)'));
  scene.add(light);
  scene.add(AmbientLight);
}

function initHelp() {
  const size = 1000;
  const divisions = 20;
  const gridHelper = new THREE.GridHelper(size, divisions);
  scene.add(gridHelper);

  // The X axis is red. The Y axis is green. The Z axis is blue.
  const axesHelper = new THREE.AxesHelper(500);
  scene.add(axesHelper);
}

function axesHelperWord() {
  let xP = addWord('X轴');
  let yP = addWord('Y轴');
  let zP = addWord('Z轴');
  xP.position.set(400, 0, 0);
  yP.position.set(0, 400, 0);
  zP.position.set(0, 0, 400);
}

function addWord(word) {
  let name = `<span>${word}</span>`;
  let moonDiv = document.createElement('div');
  moonDiv.className = 'label';
  // moonDiv.textContent = 'Moon';
  // moonDiv.style.marginTop = '-1em';
  moonDiv.innerHTML = name;
  const label = new CSS2DObject(moonDiv);
  group.add(label);
  return label;
}

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
  time += 0.05;
  if (shaderMaterial) {
    shaderMaterial.uniforms.uTime.value = time;
  }
  time1 += 0.01;
  if (boxShaderMaterial) {
    boxShaderMaterial.uniforms.uTime.value = time1;
  }
  if (shaderMaterialSphere) {
    shaderMaterialSphere.uniforms.uTime.value += 0.005;
    if (shaderMaterialSphere.uniforms.uTime.value >= 0.9) {
      shaderMaterialSphere.uniforms.uTime.value = -0.1;
    }
  }
  requestAnimationFrame(animate);
  stats.update();
  controls.update();
  labelRenderer.render(scene, camera);
  renderer.render(scene, camera);
}
</script>
</body>

</html>
相关推荐
SoraLuna21 分钟前
「Mac玩转仓颉内测版26」基础篇6 - 字符类型详解
开发语言·算法·macos·cangjie
T^T尚24 分钟前
uniapp H5上传图片前压缩
前端·javascript·uni-app
出逃日志1 小时前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
XIE3921 小时前
如何开发一个脚手架
前端·javascript·git·npm·node.js·github
GISer_Jing1 小时前
React渲染相关内容——渲染流程API、Fragment、渲染相关底层API
javascript·react.js·ecmascript
山猪打不过家猪1 小时前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山1 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
对卦卦上心1 小时前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师1 小时前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js
GIS好难学2 小时前
《Vue零基础入门教程》第二课:搭建开发环境
前端·javascript·vue.js·ecmascript·gis·web