记录--ThreeJs手搓一个罗盘特效

Posted 林恒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录--ThreeJs手搓一个罗盘特效相关的知识,希望对你有一定的参考价值。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

先上效果

前言

最近在学Three.js.,对着文档看了一周多,正好赶上码上掘金的活动,就顺便写了一个小demo,手搓一个罗盘特效。

太极

先来看一下太极的实现方式,这里我们使用CircleGeometry,将其分解开来可以看出是由圆形和半圆形组成 。

CircleGeometry

CircleGeometry官网案例
radius 半径
segments 分段(三角面)的数量
thetaStart 第一个分段的起始角度
thetaLength 圆形扇区的中心角

这里不需要用到segments,但是需要颜色,所以定义一个函数传入半径、颜色、起始角度、中心角。

const createCircle = (r, color, thetaStart, thetaLength) => 
    const material = new THREE.MeshBasicMaterial(
    color: color,
    side: THREE.DoubleSide
    );
    const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);
    const circle = new THREE.Mesh(geometry, material);
    return circle;
  ;

我们只需要通过传参生产不同大小的圆或半圆,再进行位移就可以实现其效果。

参考代码/73-96行 还有一些需要注意的地方写在注释里了。

罗盘

接下来看罗盘的实现,罗盘由一个个圆环组成,一个圆环又由内圈、外圈、分隔线、文字、八卦构成。

 

内外圈

内外圈我们使用两个RingGeometry

RingGeometry官网案例
innerRadius 内部半径
outerRadius 外部半径
thetaSegments 圆环的分段数
phiSegments 圆环的分段数
thetaStart 起始角度
thetaLength 圆心角

通过circle控制内外圆圈的尺寸,circleWidth控制圆圈的线宽

  const circleWidth = [0.1, 0.1]
  const circle = [0, 1];
  circle.forEach((i, j) => 
    const RingGeo = new THREE.RingGeometry(
      innerRing + i,
      innerRing + i + circleWidth[j],
      64,
      1
    );
    const Ring = new THREE.Mesh(RingGeo, material);
    RingGroup.add(Ring);
  );

分隔线

分隔线使用的是PlaneGeometry

PlaneGeometry官网案例
width 宽度
height 高度
widthSegments 宽度分段数
heightSegments 高度分段数

关于分隔线,它的长度就是内外圈的差值,所以这里使用外圈的数值,确定与圆心的距离就要使用内圈的数值加上自身长度除2。除此之外,还需要计算分隔线与圆心的夹角。

  for (let i = 0; i < lineNum; i++) 
    const r = innerRing + circle[1] / 2;
    const rad = ((2 * Math.PI) / lineNum) * i;
    const x = Math.cos(rad) * r;
    const y = Math.sin(rad) * r;
    const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);
    const line = new THREE.Mesh(planeGeo, material);

    line.position.set(x, y, 0);
    line.rotation.set(0, 0, rad + Math.PI / 2);
    RingGroup.add(line);
  

文字

文字使用的是TextGeometry,定位与分隔线一致,只需要交错开来。

for (let i = 0; i < lineNum; i++) 
      const r = innerRing + circle[1] / 2;
      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
      const x = Math.cos(rad) * r;
      const y = Math.sin(rad) * r;
      var txtGeo = new THREE.TextGeometry(text[i % text.length], 
        font: font,
        size: size,
        height: 0.001,
        curveSegments: 12,
      );
      txtGeo.translate(offsetX, offsetY, 0);
      var txt = new THREE.Mesh(txtGeo, material);
      txt.position.set(x, y, 0);
      txt.rotation.set(0, 0, rad + -Math.PI / 2);
      RingGroup.add(txtMesh);

不过TextGeometry的使用有一个得注意得前提,我们需要引入字体文件。

const fontLoader = new THREE.FontLoader();
const fontUrl =
  "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";
let font;
const loadFont = new Promise((resolve, reject) => 
  fontLoader.load(
    fontUrl,
    function (loadedFont) 
      font = loadedFont;
      resolve();
    ,
    undefined,
    function (err) 
      reject(err);
    
  );
);

八卦

圆环中除了文字之外,还能展示八卦,通过传递baguaData给createBagua生成每一个符号。

const baguaData = [
      [1, 1, 1],
      [0, 0, 0],
      [0, 0, 1],
      [0, 1, 0],
      [0, 1, 1],
      [1, 0, 0],
      [1, 0, 1],
      [1, 1, 0],
    ];
    for (let i = 0; i < lineNum; i++) 
      const r = innerRing + circle[1] / 2;
      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
      const x = Math.cos(rad) * r;
      const y = Math.sin(rad) * r;
      RingGroup.add(
        createBagua(baguaData[i % 8], x, y, 0 , rad + Math.PI / 2, text[0]),
      );
    

createBagua参考代码/114-146行 ,和分隔线是一样的,使用了PlaneGeometry只是做了一些位置的设置。

视频贴图

在罗盘外,还有一圈视频,这里是用到了VideoTexture,实现也很简单。唯一得注意的是视频的跨域问题,需要配置video.crossOrigin = "anonymous"

  const videoSrc = [
    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",
    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",
  ];
  video.src = videoSrc[Math.floor(Math.random() * 2)];
  video.crossOrigin = "anonymous";
  const texture = new THREE.VideoTexture(video);
  ...
  
      const material = new THREE.MeshBasicMaterial(
      color: 0xffffff,
      side: THREE.DoubleSide,
      map: texture,
    );

动画

动画总共分为三个部分,一块是旋转动画,一块是分解动画和入场动画,我们使用gsap实现。

旋转动画

  gsap.to(videoGroup.rotation, 
    duration: 30,
    y: -Math.PI * 2,
    repeat: -1,
    ease: "none",
  );

分解动画

      .to(RingGroup.position, 
        duration: 1,
        ease: "ease.inOut",
        y: Math.random() * 10 - 5,
        delay: 5,
      )
      .to(RingGroup.position, 
        duration: 1,
        ease: "ease.inOut",
        delay: 5,
        y: 0,
      )
  

入场动画

    item.scale.set(1.2, 1.2, 1.2);
    gsap.to(item.scale, 
      duration: 0.8,
      x: 1,
      y: 1,
      repeat: 0,
      ease: "easeInOut",
    );

旋转动画与分解动画可以写在生成函数内,也可以写在添加scene时,但是入场动画只能写到scene后,因为在生成时,动画就添加上了,当我们点击开始的时候才会将其加入场景中,而这时动画可能已经执行了。

 

总代码

html

<!-- 
灵感来源:一人之下里的八奇技————风后奇门,但是剧中和漫画中施展的罗盘有限,所以就参考了罗盘特效随便排布。
从抖音选取了两段剪辑随机播放。

实现方式:Three.js





 -->




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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
    <canvas class="webgl"></canvas>
    <div class="box">
        <div>大道五十,天衍四九,人遁其一</div>
        <img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e80a3fa048e84f02bb5ef5b6b04af87f~tplv-k3u1fbpfcp-no-mark:240:240:240:160.awebp?">
        <div class="btn">推衍中...</div>
    </div>
</body>

</html>

style

*
  margin: 0;
  padding: 0;

body 
  background-color: #3d3f42;


.box
  width: 350px;
  height: 250px;
  background-color: #000;
  position:absolute;
  top: calc(50% - 75px);
  left: calc(50% - 150px);
  border-radius: 10px;
  font-size: 16px;
  color: #fff;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;

.btn 
  width: 120px;
  height: 35px;
  line-height: 35px;
  color: #fff;
  border: 2px solid #fff;
  border-radius: 10px;
  font-size: 20px;
  transition: 0.5s;
  text-align: center;
  cursor:default;
  opacity: 0.5;

img
  width: 200px;
  height: 150px;

js

import * as THREE from "three@0.125.1";
import  OrbitControls  from "three/examples/jsm/controls/OrbitControls.js";
import  gsap  from "gsap@3.5.1";
// Canvas
const canvas = document.querySelector("canvas.webgl");
const box = document.querySelector(".box");
const btn = document.querySelector(".btn");
const video = document.createElement("video");

// Scene
const scene = new THREE.Scene();
 
//----------------------

const fontLoader = new THREE.FontLoader();
const fontUrl =
  "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";
let font;
const loadFont = new Promise((resolve, reject) => 
  fontLoader.load(
    fontUrl,
    function (loadedFont) 
      font = loadedFont;
      resolve();
    ,
    undefined,
    function (err) 
      reject(err);
    
  );
);
const text = 
  五行: ["金", "木", "水", "火", "土"],
  八卦: ["乾", "坤", "震", "巽", "坎", "艮", "离", "兑"],
  数字: ["壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖", "拾"],
  天干: ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"],
  地支: [
    "子",
    "丑",
    "寅",
    "卯",
    "辰",
    "巳",
    "午",
    "未",
    "申",
    "酉",
    "戌",
    "亥",
  ],
  方位: [
    "甲",
    "卯",
    "乙",
    "辰",
    "巽",
    "巳",
    "丙",
    "午",
    "丁",
    "未",
    "坤",
    "申",
    "庚",
    "酉",
    "辛",
    "戍",
    "干",
    "亥",
    "壬",
    "子",
    "癸",
    "丑",
    "艮",
    "寅",
  ],
  节气: [
    "立  春",
    "雨  水",
    "惊  蛰",
    "春  分",
    "清  明",
    "谷  雨",
    "立  夏",
    "小  满",
    "芒  种",
    "夏  至",
    "小  暑",
    "大  暑",
    "立  秋",
    "处  暑",
    "白  露",
    "秋  分",
    "寒  露",
    "霜  降",
    "立  冬",
    "小  雪",
    "大  雪",
    "冬  至",
    "小  寒",
    "大  寒",
  ],
  天星: [
    "天辅",
    "天垒",
    "天汉",
    "天厨",
    "天市",
    "天掊",
    "天苑",
    "天衡",
    "天官",
    "天罡",
    "太乙",
    "天屏",
    "太微",
    "天马",
    "南极",
    "天常",
    "天钺",
    "天关",
    "天潢",
    "少微",
    "天乙",
    "天魁",
    "天厩",
    "天皇",
  ],
  天干1: [
    "甲",
    " ",
    "乙",
    " ",
    "丙",
    " ",
    "丁",
    " ",
    "戊",
    " ",
    "己",
    " ",
    "庚",
    " ",
    "辛",
    " ",
    "壬",
    " ",
    "癸",
    " ",
    "甲",
    " ",
    "乙",
    " ",
  ],
  地支1: [
    "子",
    " ",
    "丑",
    " ",
    "寅",
    " ",
    "卯",
    " ",
    "辰",
    " ",
    "巳",
    " ",
    "午",
    " ",
    "未",
    " ",
    "申",
    " ",
    "酉",
    " ",
    "戌",
    " ",
    "亥",
    " ",
  ],
;
const data = [
  
    innerRing: 2,
    outerRing: 1.5,
    lineWidth: 0.1,
    circleWidth: [0.1, 0.1],
    lineNum: 8,
    text: [0xffffff],
    offsetX: 0,
    offsetY: 0,
    size: 0.3,
    direction: -1,
    duration: 40,
  ,
  
    innerRing: 3.5,
    outerRing: 0.7,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 24,
    text: text["方位"],
    offsetX: -0.2,
    offsetY: -0.08,
    size: 0.3,
    direction: 1,
    duration: 10,
  ,
  
    innerRing: 4.2,
    outerRing: 0.7,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 24,
    text: text["八卦"],
    offsetX: -0.2,
    offsetY: -0.08,
    size: 0.3,
    direction: -1,
    duration: 20,
  ,
  
    innerRing: 4.9,
    outerRing: 1.3,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 24,
    text: text["方位"],
    offsetX: -0.4,
    offsetY: -0.2,
    size: 0.6,
    direction: 1,
    duration: 30,
  ,
  
    innerRing: 6.2,
    outerRing: 0.4,
    lineWidth: 0.15,
    circleWidth: [0, 0],
    lineNum: 60,
    text: text["地支"],
    offsetX: -0.13,
    offsetY: 0.01,
    size: 0.2,
    direction: 1,
    duration: 25,
  ,
  
    innerRing: 6.6,
    outerRing: 0.4,
    lineWidth: 0.15,
    circleWidth: [0, 0],
    lineNum: 60,
    text: text["天干"],
    offsetX: -0.13,
    offsetY: -0.07,
    size: 0.2,
    direction: 1,
    duration: 25,
  ,
  
    innerRing: 7,
    outerRing: 0.5,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 36,
    text: text["天星"],
    offsetX: -0.27,
    offsetY: -0.03,
    size: 0.2,
    direction: -1,
    duration: 20,
  ,
  
    innerRing: 7.5,
    outerRing: 0.5,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 24,
    text: text["节气"],
    offsetX: -0.36,
    offsetY: -0.03,
    size: 0.2,
    direction: 1,
    duration: 30,
  ,
  
    innerRing: 8,
    outerRing: 0.8,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 48,
    text: text["方位"],
    offsetX: -0.3,
    offsetY: -0.1,
    size: 0.4,
    direction: 1,
    duration: 35,
  ,
  
    innerRing: 8.8,
    outerRing: 0.8,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 32,
    text: text["八卦"],
    offsetX: -0.3,
    offsetY: -0.1,
    size: 0.4,
    direction: -1,
    duration: 60,
  ,
  
    innerRing: 9.6,
    outerRing: 0.4,
    lineWidth: 0.18,
    circleWidth: [0, 0],
    lineNum: 120,
    text: text["地支1"],
    offsetX: -0.13,
    offsetY: 0.01,
    size: 0.2,
    direction: 1,
    duration: 30,
  ,
  
    innerRing: 10,
    outerRing: 0.4,
    lineWidth: 0.18,
    circleWidth: [0, 0],
    lineNum: 120,
    text: text["天干1"],
    offsetX: -0.13,
    offsetY: -0.07,
    size: 0.2,
    direction: 1,
    duration: 30,
  ,
  
    innerRing: 10.4,
    outerRing: 0.5,
    lineWidth: 0.1,
    circleWidth: [0.1, 0.1],
    lineNum: 60,
    text: text["数字"],
    offsetX: -0.13,
    offsetY: -0.02,
    size: 0.2,
    direction: 1,
    duration: 25,
  ,
  
    innerRing: 10.9,
    outerRing: 0.5,
    lineWidth: 0.15,
    circleWidth: [0.1, 0.1],
    lineNum: 50,
    text: text["五行"],
    offsetX: -0.13,
    offsetY: -0.02,
    size: 0.2,
    direction: 1,
    duration: 35,
  ,
  
    innerRing: 11.7,
    outerRing: 1,
    lineWidth: 0.1,
    circleWidth: [1, 0],
    lineNum: 64,
    text: [0x000000],
    offsetX: 0,
    offsetY: 0,
    size: 0.3,
    direction: 1,
    duration: 30,
  ,
];
const Rings = [];
const duration = [
  0, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7,
];

//Ring
const Ring = (
  innerRing,
  outerRing,
  lineWidth,
  circleWidth,
  lineNum,
  offsetX,
  offsetY,
  text,
  size,
  direction,
  duration,
) => 
  const RingGroup = new THREE.Group();
  const circle = [0, outerRing];
  const material = new THREE.MeshStandardMaterial(
    color: 0xffffff,
    side: THREE.DoubleSide,
  );

  // create ring
  circle.forEach((i, j) => 
    const RingGeo = new THREE.RingGeometry(
      innerRing + i,
      innerRing + circleWidth[j] + i,
      64,
      1
    );
    const Ring = new THREE.Mesh(RingGeo, material);
    RingGroup.add(Ring);
  );

  // create line
  for (let i = 0; i < lineNum; i++) 
    const r = innerRing + circle[1] / 2;
    const rad = ((2 * Math.PI) / lineNum) * i;
    const x = Math.cos(rad) * r;
    const y = Math.sin(rad) * r;
    const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);
    const line = new THREE.Mesh(planeGeo, material);

    line.position.set(x, y, 0);
    line.rotation.set(0, 0, rad + Math.PI / 2);
    RingGroup.add(line);
  

  // create text
  if (text.length > 1) 
    for (let i = 0; i < lineNum; i++) 
      const r = innerRing + circle[1] / 2;
      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
      const x = Math.cos(rad) * r;
      const y = Math.sin(rad) * r;
      var txtGeo = new THREE.TextGeometry(text[i % text.length], 
        font: font,
        size: size,
        height: 0.001,
        curveSegments: 12,
      );
      txtGeo.translate(offsetX, offsetY, 0);
      var txtMater = new THREE.MeshStandardMaterial( color: 0xffffff );
      var txtMesh = new THREE.Mesh(txtGeo, txtMater);
      txtMesh.position.set(x, y, 0);
      txtMesh.rotation.set(0, 0, rad + -Math.PI / 2);
      RingGroup.add(txtMesh);
    
  

  // create bagua
  if (text.length == 1) 
    const baguaData = [
      [1, 1, 1],
      [0, 0, 0],
      [0, 0, 1],
      [0, 1, 0],
      [0, 1, 1],
      [1, 0, 0],
      [1, 0, 1],
      [1, 1, 0],
    ];
    for (let i = 0; i < lineNum; i++) 
      const r = innerRing + circle[1] / 2;
      const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
      const x = Math.cos(rad) * r;
      const y = Math.sin(rad) * r;
      RingGroup.add(
        createBagua(baguaData[i % 8], x, y, 0.0001, rad + Math.PI / 2, text[0]),
        createBagua(baguaData[i % 8], x, y, -0.0001, rad + Math.PI / 2, text[0])
      );
    
  

  // animation
  
    gsap.to(RingGroup.rotation, 
      duration: duration,
      z: Math.PI * 2 * direction,
      repeat: -1,
      ease: "none",
    );
    
    const amColor =  r: 1, g: 1, b: 1 ;
    const explode = gsap.timeline( repeat: -1, delay: 5 );
    explode
      .to(RingGroup.position, 
        duration: 1,
        ease: "ease.inOut",
        y: Math.random() * 10 - 5,
        delay: 5,
      )
      .to(amColor, 
        r: 133 / 255,
        g: 193 / 255,
        b: 255 / 255,
        duration: 2,
        onUpdate: () =>
          ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),
      )
      .to(RingGroup.position, 
        duration: 1,
        ease: "ease.inOut",
        delay: 5,
        y: 0,
      )
      .to(amColor, 
        r: 1,
        g: 1,
        b: 1,
        duration: 3,
        onUpdate: () =>
          ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),
      );
  

  // rotate
  RingGroup.rotateX(-Math.PI / 2);
  return RingGroup;
;

//taiji
const createTaiji = (position, scale) => 
  const taiji = new THREE.Group();
  const createCircle = (r, color, thetaStart, thetaLength) => 
    const material = new THREE.MeshBasicMaterial(
      color: color,
      side: THREE.DoubleSide,
    );
    const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);
    const circle = new THREE.Mesh(geometry, material);
    return circle;
  ;

  const ying = createCircle(1.8, 0x000000, 0, Math.PI);
  const yang = createCircle(1.8, 0xffffff, Math.PI, Math.PI);
  const Lblack = createCircle(0.9, 0x000000, 0, Math.PI * 2);
  const Lwhite = createCircle(0.9, 0xffffff, 0, Math.PI * 2);
  const Sblack = createCircle(0.25, 0x000000, 0, Math.PI * 2);
  const Swhite = createCircle(0.25, 0xffffff, 0, Math.PI * 2);

  const Lblack1 = createCircle(0.9, 0x000000, 0, Math.PI * 2);
  const Lwhite1 = createCircle(0.9, 0xffffff, 0, Math.PI * 2);
  const Sblack1 = createCircle(0.25, 0x000000, 0, Math.PI * 2);
  const Swhite1 = createCircle(0.25, 0xffffff, 0, Math.PI * 2);

  Lblack.position.set(-0.9, 0, 0.001);
  Lwhite.position.set(0.9, 0, 0.001);
  Swhite.position.set(-0.9, 0, 0.002);
  Sblack.position.set(0.9, 0, 0.002);
  Lblack1.position.set(-0.9, 0, -0.001);
  Lwhite1.position.set(0.9, 0, -0.001);
  Swhite1.position.set(-0.9, 0, -0.002);
  Sblack1.position.set(0.9, 0, -0.002);

  taiji.add(
    ying,
    yang,
    Lblack,
    Lwhite,
    Swhite,
    Sblack,
    Lblack1,
    Lwhite1,
    Swhite1,
    Sblack1
  );
  gsap.to(taiji.rotation, 
    duration: 30,
    z: Math.PI * 2,
    repeat: -1,
    ease: "none",
  );
  taiji.rotateX(-Math.PI / 2);
  taiji.position.set(...position);
  taiji.scale.set(...scale);
  return taiji;
;
scene.add(createTaiji([0, 0, 0], [1, 1, 1]));

// bagua
const createBagua = (data, x, y, z, deg, color) => 
  const idx = [-0.32, 0, 0.32];
  const bagua = new THREE.Group();
  const material = new THREE.MeshStandardMaterial(
    color: color,
    side: THREE.DoubleSide,
  );
  data.forEach((i, j) => 
    if (i == 1) 
      const yang = new THREE.Mesh(new THREE.PlaneGeometry(1, 0.2), material);
      yang.position.set(0, idx[j], 0);
      bagua.add(yang);
    
    if (i == 0) 
      const ying1 = new THREE.Mesh(
        new THREE.PlaneGeometry(0.45, 0.2),
        material
      );
      const ying2 = new THREE.Mesh(
        new THREE.PlaneGeometry(0.45, 0.2),
        material
      );
      ying1.position.set(-0.275, idx[j], 0);
      ying2.position.set(0.275, idx[j], 0);
      bagua.add(ying1, ying2);
    
  );
  bagua.position.set(x, y, z);
  bagua.rotation.set(0, 0, deg);
  return bagua;
;


const showVideo = () => 
  const videoSrc = [
    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",
    "https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",
  ];
  video.src = videoSrc[Math.floor(Math.random() * 2)];
  video.crossOrigin = "anonymous";
  const texture = new THREE.VideoTexture(video);
  const videoGroup = new THREE.Group();
  for (let i = 0; i < 8; i++) 
    const r = 25;
    const rad = ((2 * Math.PI) / 8) * i;
    const x = Math.cos(rad) * r;
    const y = Math.sin(rad) * r;
    const planeGeo = new THREE.PlaneGeometry(16, 9);
    const material = new THREE.MeshBasicMaterial(
      color: 0xffffff,
      side: THREE.DoubleSide,
      map: texture,
    );
    const plane = new THREE.Mesh(planeGeo, material);

    plane.position.set(x, 4.5, y);
    if (i % 2 == 0) plane.rotation.set(0, rad + Math.PI / 2, 0);
    else plane.rotation.set(0, rad, 0);
    videoGroup.add(plane);
  
  gsap.to(videoGroup.rotation, 
    duration: 30,
    y: -Math.PI * 2,
    repeat: -1,
    ease: "none",
  );
  scene.add(videoGroup);
;

//loadFont, Rings
loadFont.then(() => 
  data.forEach((item) => 
    Rings.push(Ring(item));
  );
  btn.innerText = "入 局";
  btn.style.opacity = 1;
  btn.style.cursor = "pointer";
);

//start
const start = function () 
  const showRing = (item) => 
    scene.add(item);
    item.scale.set(1.2, 1.2, 1.2);
    gsap.to(item.scale, 
      duration: 0.8,
      x: 1,
      y: 1,
      repeat: 0,
      ease: "easeInOut",
    );
  ;
  const tl = gsap.timeline();
  Rings.forEach((item, idx) => 
    tl.to(".webgl",  duration: duration[idx] ).call(() => 
      showRing(item);
    );
  );
;



btn.addEventListener("click", () => 
  box.style.display = "none";
  start();
  showVideo();
  video.play();
  video.loop = true;
);


//----------------------
 

//Light
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
 

//Sizes
const sizes = 
  width: window.innerWidth,
  height: window.innerHeight,
;


// Camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  1,
  1000
);
camera.position.y = 10;
camera.position.x = 10;
camera.position.z = 10;
camera.lookAt(scene.position);
scene.add(camera);


//Renderer
const renderer = new THREE.WebGLRenderer(
  canvas: canvas,
  antialias: true,
  alpha: true,
);

renderer.setSize(sizes.width, sizes.height);


//controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.maxDistance = 50;
controls.enablePan = false;

const tick = () => 
  renderer.render(scene, camera);
  controls.update();
  window.requestAnimationFrame(tick);
;
tick();


window.addEventListener("resize", () => 
  sizes.height = window.innerHeight;
  sizes.width = window.innerWidth;

  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(window.devicePixelRatio);
);

本文转载于:

https://juejin.cn/post/7220629398965108794

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

ThreeJS之动画交互逻辑及特效

 工作需要,研究了一下 threejs 简单逻辑动画交互方法。写了一个小示例,分享一下,挺丑的。

第一步

当然就是初始化 threejs 的渲染场景了。

var camera; //相机
var scene;//场景
var renderer;//webGL渲染器
var controls;//轨道控件,用于特定场景,模拟轨道中的卫星,可以用鼠标和键盘在场景中游走 
var raycaster;//THREE.Raycaster对象从屏幕上的点击位置想场景中发射一束光线,返回射线穿透物体的数组
var composer;//后期特效合成器,给场景选中物体添加发光特效

第二步

在 ThreeJs Editor 中建立简单的示例模型,“Export Scene”,导出。并导入示例程序。免去了在示例程序中自己建模的麻烦,不过因为示例程序要加载本地的json,所以可以设置一个简单的 nodejs 服务器。

在 nodejs 的 anywhere 下运行该示例:

加载模型文件,将文件中的相关 object 加入 group 中:

          var url = \'nofloor.json\';
                var loader= new THREE.ObjectLoader();
          var geometry = new THREE.Geometry();//存放objects的position坐标,为之后线条的起始点坐标服务 loader.load( url,
function ( loadedScene ) { //scene = loadedScene; var objects = loadedScene.children; for(var i=0;i<objects.length;i++){ if(objects[i].type == \'Mesh\' ){ objects[i].receiveShadow = true; objects[i].castShadow = true; geometry.vertices.push(objects[i].position); group.add(objects[i]); } } } , onProgress, onError);

导入的模型差不多就是这样子(丑一点,担待),并在示例程序中加了stats 和 dat.gui 用来检测渲染效果和改变特效参数。

 第三步

完成的交互目标是,点击上图中某个柱体选中,出现相应的连线,并且让选中的柱体和连线发光。

现在先利用 raycaster 选中物体:

var mouse = new THREE.Vector2(); //鼠标经过或者点击的屏幕 canvas 上的位置           
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;//将 canvas 坐标系转换为 WebGL 坐标系
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );// raycaster的作用场景
var intersects = raycaster.intersectObjects( [group], true );//从鼠标 mouse 位置发射线,选中 group 组中的objects ,并返回 objects 给 intersects 

一旦 intersects  不为空,intersects[0].object 就是鼠标选中的物体,可以是上图中的正方体,也可以是上图中的地板。

接下来,皆可以根据选中物体来连线了。连线呢有两种方法。

第一种

代码如下:

var material = new THREE.LineBasicMaterial({
    color: 0x0000ff
});

var geometry = new THREE.Geometry();
geometry.vertices.push(
    //各个柱体的 position 坐标,也就是上面加载模型文件时候生成的 geometry.vertices
 );
// THREE.Line 会将 geometry.vertices 中所有坐标点连成一条连续线,但不是首尾相接。
//比如 geometry.vertices 中存放了 v1,v2,v3,v4四个三维坐标点,就会生成v1->v2->v3->v4 连续线,中间有三条线。
var line = new THREE.Line( geometry, material );

但是有一个缺点就是由于受限于 角度层(ANGLE layer),在Windows平台上使用 WebGL,线宽将总是为1而不管设置的值。这样一旦模型是五六米高,可是线条只有不到1cm的宽度,看起来模型就会很奇怪了。这时候就要用第二种方法。

第二种

画圆柱体,用圆柱体代替线段,但是生成圆柱体就比线段复杂多了。大家知道,threejs 只会指定一个object的position(中心),而不能指定两端的位置(我还没发现,若有错误,请指正),所以画的初始圆柱体是直立的,如下图

让我们要的是下图这样的圆柱体(下面统称为“柱子线”)

因为半径是 0.02,所以看起来像线段,这也是为什么要用圆柱体模拟线段的原因了,逼真,而且还能根据模型大小调整这个柱子线的“linewidth”。

 下面我们看看怎么怎么根据正方体个球体的坐标动态生成柱子线吧。

上面我们讲了如何选中物体,选中之后,我们把这个物体相邻的物体(建模时候,将所有物体坐标顺序放在 geometry.vertices 中,此处默认坐标相邻就是物体相邻)的 position 坐标加入 geometryChange.vertices  中

               var object = intersects[0].object;
                        geometryChange = new THREE.Geometry();
                        var position = intersects[0].object.position;//当前选中物体的坐标
                        //搜索 geometry.vertices 中的 position 重新绘制选中物体相关linet
                        var p = geometry.vertices.length;
                        for(i=0;i<p;i++){
                            if(geometry.vertices[i] == position){

                                if (i == p - 1){//将最后一个物体的前一个物体坐标加入
                                    geometryChange.vertices.push(geometry.vertices[p - 2]);
                                    geometryChange.vertices.push(position);
                                }
                                else if(i == 0){//将第一个物体的后一个坐标加入
                                    geometryChange.vertices.push(position);
                                    geometryChange.vertices.push(geometry.vertices[i+1]);
                                }

                                else{
                                    geometryChange.vertices.push(geometry.vertices[i-1]);
                                    geometryChange.vertices.push(position);//将物体前后相邻的加入
                                    geometryChange.vertices.push(geometry.vertices[i+1]);
                                  
                                }
                            }
                        }

这样我们就把选中物体相邻的物体坐标放在 geometryChange.vertices。现在我们知道柱子线的起点和终点坐标了,那柱子线怎么画,画哪里呢?

              var temp = geometryChange.vertices.length;
                        var xyz = geometryChange.vertices;
               //position(x,y,z),就是柱子线的中点位置,xw是起点和中点的 X 轴方向距离,zh是起点和中点的 Z 轴方向距离,cheight是起点和中点的空间距离
               var x,y,z,xw,zh,cheight;

先知道柱子线的position(x,y,z),xyz[i]是柱子线起点,xyz[i+1]是柱子线终点。

x= (xyz[i].x+xyz[i+1].x)/2;
y=0.1;//线我是画在地面附近的,所以y默认0.1
z=(xyz[i].z+xyz[i+1].z)/2

再来求柱子线的长度

xw=xyz[i].x-xyz[i+1].x;
zh=xyz[i].z-xyz[i+1].z;
cheight=Math.sqrt(xw*xw+zh*zh);//圆柱体长度,勾股定理

这下画柱子线

var material = new THREE.MeshPhongMaterial( {
                                        color: 0x156289,
                                        emissive: 0x00FFFF,
                                        side: THREE.DoubleSide,
                                        shading: THREE.FlatShading,
                                        vertexColors:THREE.FaceColors
                                    } );
var cylinder = new THREE.Mesh( geometryCylinderLine, material );
cylinder.position.set( x, y, z );//两实体的中点,也就是柱子线的中点,自己理解

可是发现画的柱子是竖直向上的

这个时候就需要改变柱子线的模型矩阵的,对它做旋转,达到我们理想的效果。

我们先分析一下怎么旋转,首先将绕 x 轴转90° ,让柱子线躺地上。

cylinder.rotation.x -= Math.PI * 0.5;

之后如下图分析所示,红线就是躺地上的柱子(自行脑补3D场景)。

其中红线和黑线长度相同,红线只需要旋转 θ 角度之后就可以和黑线重合,达到我们要的效果。

θ = Math.asin(xw/cheight);//弧度制

这个时候知道转多少度了,转就ok

                 //考虑到局部坐标系和全局坐标系的转换,柱体是在全局坐标系下旋转
                            if(xyz[i].x > xyz[i+1].x && xyz[i].z < xyz[i+1].z)
                                cylinder.rotation.z -= Math.asin(xw/cheight);//Math.asin(xw/cheight)为柱体要旋转的角度
                            else if(xyz[i].x > xyz[i+1].x && xyz[i].z > xyz[i+1].z)
                                cylinder.rotation.z += Math.asin(xw/cheight);
                            else if(xyz[i].x < xyz[i+1].x && xyz[i].z < xyz[i+1].z)
                                cylinder.rotation.z -= Math.asin(xw/cheight);
                            else
                                cylinder.rotation.z += Math.asin(xw/cheight);

好了,柱子线画出来了。

当然不止一条柱子线,把当前的都加入lineGroup 中

lineGroup.add( cylinder );
scene.add( lineGroup );

 下次绘制的时候只要移除当前的 lineGroup 即可。

scene.remove( lineGroup );

 

第四步

加发光特效

借助threejs的 outlinePass 通道

          composer = new THREE.EffectComposer( renderer );

                var renderPass = new THREE.RenderPass( scene, camera );
                composer.addPass( renderPass );

                outlinePass = new THREE.OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), scene, camera );
                composer.addPass( outlinePass );

                var onLoad = function ( texture ) {

                    outlinePass.patternTexture = texture;
                    texture.wrapS = THREE.RepeatWrapping;
                    texture.wrapT = THREE.RepeatWrapping;

                };

                var loader = new THREE.TextureLoader();

                loader.load( \'tri_pattern.jpg\', onLoad );

                effectFXAA = new THREE.ShaderPass( THREE.FXAAShader );
                effectFXAA.uniforms[ \'resolution\' ].value.set( 1 / window.innerWidth, 1 / window.innerHeight );
                effectFXAA.renderToScreen = true;
                composer.addPass( effectFXAA );

在选中时,将物体和相应柱子线加入  outlinePass  渲染目标中即可。

               selectedObjects = [];
                        selectedObjects.push( lineGroup );//给选中的线条和物体加发光特效
                        selectedObjects.push( intersects[ 0 ].object );
                        outlinePass.selectedObjects = selectedObjects;

ok,这就实现了,点击交互的简单特效。

当然,这只是个示例,要把它用到复杂的3D场景中,还需要很多的事情要做,加油。

 有错误敬请指正。

以上是关于记录--ThreeJs手搓一个罗盘特效的主要内容,如果未能解决你的问题,请参考以下文章

threejs 反锯齿,raser,特效发光

vue中使用threejs仿iView官网大波浪特效

threejs效果 Thanos Portal特效

threejs效果 Thanos Portal特效

threejs效果 Thanos Portal特效

Threejs 精灵火焰特效 Sprite Firey Aura effect