Three.js 初体验

这是一个小尝试,需要展示一个动态的 3d 模型,实现一个轨道检测车模型,主要是控制四个车轮的转动动画。后来加入点击事件、鼠标移入事件、动画控制等,我就把我遇到过的问题一一说一下吧。PS:写的原生。

初始化场景

//场景
let scene = new THREE.Scene();
//相机设置为世界坐标原点
let camera = null;
let axes = null;
let contentWidth = 0;
let contentHeight = 0;

// 初始化场景
function initScene() {
  contentWidth = document.getElementById( 'threeContent' ).clientWidth;
  contentHeight = document.getElementById( 'threeContent' ).clientHeight;
  camera = new THREE.PerspectiveCamera(60, contentWidth / contentHeight, 0.1, 1000);
  camera.position.set(8, 2, -6.5);
  // 看向 0,0,0
  camera.lookAt(scene.position);
  scene.add(camera);//添加相机
  //添加坐标轴
  axes = new THREE.AxesHelper(500);//500表示xyz轴的长度,红:x,绿:y,蓝:z
  light = new THREE.AmbientLight(0x000000); // 打光
  scene.fog = new THREE.Fog( 0x333333, 10, 18 );
  renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
  });//画布
  renderer.setSize((contentWidth - 4), contentHeight);//设置渲染区域尺寸
  // renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
  renderer.setClearColor(0xb9d3ff, .8);
  renderer.shadowMap.enabled = true;
}
async function addGlb() {
  const loader = new THREE.GLTFLoader();
  const dracoloader = getDracoLoader();
  loader.setDRACOLoader(dracoloader);//注入loader
  //加车辆
  scene.name = loadModel.car_no;
  scene.add(axes);
  scene.add(light);
  const floor = await loadGlb(loadModel.model, loader);
  floor.position.y = 0;
  scene.add(floor);
}

加载车模型

我使用的是 .glb 文件,因为要控制车轮(wheels),铁轨(lines),还有警灯(alarms)的变化,所以我们在拿到模型后,要先把这些对象拿出来,同时,因为窗是我自己加上的,为了能让模型看着更真实一点,我们也可以在此时再次设置模型的材料。最后再把模型加到场景中去。

// 加载glb文件,同时对模型做处理
function loadGlb(filepath, loader) {
  wheels = [];
  lines = [];
  return new Promise((resolve, reject) => {
    loader.setCrossOrigin('Anonymous');//跨域问题
    loader.load(filepath, async (glb) => {
      const carModel = glb.scene;
      const root = carModel.getObjectByName( 'root' );
      root.getObjectByName('glass').material = glassMaterial;
      root.getObjectByName('bodySTL').material = bodyMaterial;
      // 铁轨
      lines = root.getObjectByName('h_lines').children.slice();
      // 车轮
      wheels = carModel.getObjectByName('wheels').children.slice();
      // 警灯
      carModel.getObjectByName('alarm').material = glassMaterial;
      alarms = carModel.getObjectByName('alarm');
      scene.add( carModel );
      const canvas = getTextCanvas(loadModel.car_no);
      createTextPlane(canvas);
      //处理材质丢失的情况
      glb.scene.traverse(child => {
        if (child.isMesh) {
          child.material.emissive = child.material.color;
          child.material.emissiveMap = child.material.map;
        }
      });
      resolve(glb.scene);
    }, undefined, (error) => {
      console.error(error);
      reject(error);
    });
  });
}

车轮旋转 / 铁轨 动画

对于时间的计算,可以使用 performance.now 或者 Three.js 自带的 Clock API 来实现,然后执行一个动画,可以使用 requestAnimationFrame 来实现:

  • Clock:对象用于跟踪时间。如果 performance.now 可用,则 Clock 对象通过该方法实现,否则回落到使用略欠精准的 Date.now 来实现。
  • window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

以下我使用两种方式举例分别实现车轮和铁轨动画:

// 用于存储动画对象,可以在暂停的时候
let animates = null;
const clock = new THREE.Clock();
// 车轮、铁轨动画效果
function animate() {
  animates = requestAnimationFrame(animate);
  const time = - performance.now() / 1000;
  for ( let i = 0; i < wheels.length; i ++ ) {
    // 根据正确的轴选择
    wheels[ i ].rotation.x = time * Math.PI * 2;
  }
  // 铁轨动画
  const delta = clock.getDelta();
  const distance = - delta * 0.75;
  for ( let i = 0; i < lines.length; i ++ ) {
    // 根据正确的轴移动,模型的间距是相等的,保证最末位置和最初位置的变化
    // 暂停又播放的时候 lines[ i ].position.z + distance <= -(lines.length)*1.8 来保证重新播放时铁轨间距不发生混乱
    if (lines[ i ].position.z <= -(lines.length)*1.8  && lines[ i ].position.z + distance <= -(lines.length)*1.8) {
      lines[ i ].position.z = 0;
    } else {
      lines[ i ].position.z += distance;
    }
  }
  renderer.clear();
  controls.update();
  render();
}
// 开始渲染
function render() {
  renderer.render(scene, camera);
}
// 暂停动画
function handleAnimation(action) {
  if (action === 'stop') {
    window.cancelAnimationFrame(animates);
    animates = null;
  } else {
    animate();
  }
}

场景鼠标控制

使用的是 OrbitControls(轨道控制器)可以使得相机围绕目标进行轨道运动。

这个地方踩了俩坑,一个是设置可控水平旋转角度(上限下限一定要同时设置),另一个是模型在静态的时候,鼠标控制失效(controls.addEventListener( 'change', render );

官方文档:three.js docs

// 添加场景鼠标控制
let controls = null;
function createCot() {
  // 鼠标控制事件 移动、缩放、旋转
  controls = new THREE.OrbitControls(camera, renderer.domElement);
  // 这是控制镜头移至的最远距离
  controls.maxDistance = 16;
  // 控制器的焦点
  controls.target.set( 0, 0.5, 0 );
  // 能够水平旋转的角度上限
  controls.maxAzimuthAngle = 1 * Math.PI;
  // 能够水平旋转的角度下限
  controls.minAzimuthAngle = 0;
  // 能够垂直旋转的角度的上限
  controls.maxPolarAngle = 0.5 * Math.PI;
  // 旋转的速度
  controls.rotateSpeed = 0.4;
  // 位移的速度
  controls.panSpeed = 0.4;
  // 模型静态时控制关键
  controls.addEventListener('change', render);
  //将渲染好的canvas追加到dom
  let cont = document.getElementById('webgl');
  cont.appendChild(renderer.domElement);
}

纹理贴图(文字、图片)

  • 选择 TextureLoader 创建一个纹理贴图,将其应用到一个表面,或者作为反射/折射贴图:three.js docs
    • 这里一定要注意,图片加载是异步的,如果出现图片未展示的情况,可能是图片还未加载完成
  • 选择 CanvasTexture 从 Canvas 元素中创建纹理贴图。它几乎与其基类 Texture 相同,但它直接将 needsUpdate(需要更新)设置为了 true
// logo贴图
async function createLogoPlane() {
  const geometry = new THREE.PlaneGeometry(0.5, 0.5, 1);
  const textureLoader = new THREE.TextureLoader(); // 纹理加载器
  const texture = await textureLoader.load('./threejs/res/models/gltf/railway.png'); // 加载图片,返回Texture对象
  const material = new THREE.MeshBasicMaterial({
    map: texture, // 设置纹理贴图
    //设置深浅程度,默认值(1,1)。
    side: THREE.DoubleSide,  // 双面
    transparent: true,
  });
  const mesh1 = new THREE.Mesh(geometry, material);
  const mesh2 = new THREE.Mesh(geometry, material);
  mesh1.rotation.y = Math.PI * 0.5;
  mesh2.rotation.y = Math.PI * 0.5;
  mesh1.position.set(0.643,1.189, -4.496);
  mesh2.position.set(3.192,1.189, -4.496);
  scene.add(mesh1, mesh2);
}
// 文字贴图(车号)
function createTextPlane(canvas) {
  const geometry = new THREE.PlaneGeometry(0.6, 0.3, 1);
  const texture = new THREE.CanvasTexture(canvas);  // canvas做画布
  const material = new THREE.MeshBasicMaterial({
    map: texture, // 设置纹理贴图
    side: THREE.DoubleSide,  // 双面
    transparent: true,
  });
  const mesh = new THREE.Mesh(geometry, material);
  const mesh2 = new THREE.Mesh(geometry, material);
  mesh.rotation.y = Math.PI * 0.5;
  mesh2.rotation.y = Math.PI * 0.5;
  mesh.position.set(0.643,0.969, -4.566);
  mesh2.position.set(3.192,0.969, -4.566);
  scene.add(mesh, mesh2);
}
//  文字纹理--创建文字canvas
function getTextCanvas(text){ 
  let width=60, height=30; 
  let canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  let ctx = canvas.getContext('2d');
  ctx.fillStyle = '#dbce8f00';
  ctx.fillRect(0, 0, width, height);
  ctx.font = 18 + 'px " bold';
  ctx.textBaseline = "middle"; //文本与fillText定义的纵坐标
  // ctx.textAlign = "left";
  ctx.fillStyle = '#b13e21';
  ctx.fillText(text,2,16);
  return canvas;
}

不过最后 logo 贴图我选择了直接在 threejs 的编辑器里贴的,一定要把 transparent 勾选上

添加警灯(呼吸灯)

  • 效果合成器(EffectComposer):用于在 three.js 中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。
  • 自发光(BloomPass

我这里当时用的 setInterval 和 requestAnimationFrame 实现的呼吸效果,后面忙别的去了暂时没有优化代码了


// 警灯
let alarms = null;
let alarmAni = null;
let alarmLight = null;
let lightMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
let lightTimer = null;
let originalColor = new THREE.Color(0xbbbbbb);
// 效果渲染器
let composer = null;
// 警灯动画效果
function alarmAnimate() {
  alarmAni = requestAnimationFrame(alarmAnimate);
  composer.render();
  render();
}
function cancelAlarmAni() {
  alarmAni = null;
  clearInterval(lightTimer);
  lightTimer = null;
  lightMaterial.emissive = new THREE.Color(0xbbbbbb);
  lightMaterial.emissiveIntensity = 1;
}
//高亮显示模型(呼吸灯)
function outlineObj () {
  // 创建一个Pass,用于实现车灯的自发光
  let bloomPass = new THREE.BloomPass(new THREE.Vector2( contentWidth, contentHeight), 25, 4, 256);
  lightMaterial.emissive = new THREE.Color(0xff0000);
  alarms.children.forEach((item) => {
    item.material = lightMaterial;
  });
  // 定义定时器,控制车灯的闪烁
  lightTimer = setInterval(function() {
    const time = performance.now() / 1000;
    lightMaterial.emissiveIntensity = Math.sin(time * 4) * 0.5 + 0.5; // 随机修改材质的自发光强度,实现闪烁效果
  }, 100);
  // 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
  composer = new THREE.EffectComposer(renderer);
  // 新建一个场景通道  为了覆盖到原理来的场景上
  let renderPass = new THREE.RenderPass(scene, camera);
  composer.addPass(renderPass);
  // 添加一个ShaderPass,用于将车灯的自发光添加到场景中
  const shaderPass = new THREE.ShaderPass(THREE.CopyShader);
  shaderPass.renderToScreen = true;
  composer.addPass(bloomPass);
  composer.addPass(shaderPass);
}

给车轮添加点击事件和鼠标 hover 事件

  • Vector2 :Three.js 中的二维向量,表示2D vector(二维向量)的类
  • Raycaster : 射线,利用光线投射实现鼠标拾取
    • 我们利用在标准化设备坐标中鼠标的二维坐标与射线所来源的摄像机引入射线(setFromCamera),检查与射线相交的物体(intersectObjects),检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。
// 交互 - 通过射线找到对应交互的物体
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
// hover 事件
function onHoverMesh(event) {
  event.preventDefault();
  mouse.x =((event.offsetX) / renderer.domElement.clientWidth) * 2 - 1;
  mouse.y =-((event.offsetY) / renderer.domElement.clientHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  // 获取点击到的车轮
  let intersects = raycaster.intersectObjects(wheels, true);
  //当intersects.length > 0说明碰撞到物体,获取最近的物体名称进行判断
  console.log('hover>>>>>>>>>', intersects);
  if (intersects.length > 0) {
    // 碰到到的车轮
    // console.log(intersects[ 0 ].object, 'intersects[ 0 ].object');
    if (INTERSECTED == null || INTERSECTED.name != intersects[ 0 ].object.name ) {
      if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
      INTERSECTED = intersects[ 0 ].object;
      INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
      INTERSECTED.material.emissive.setHex( 0xff8800 );
    }
  } else {
    if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
    INTERSECTED = null;
  }
}
// click 事件
function onClickMesh(event) {
  event.preventDefault();
  console.log(event, 'click---------------<');
  mouse.x =((event.offsetX) / renderer.domElement.clientWidth) * 2 - 1;
  mouse.y =-((event.offsetY) / renderer.domElement.clientHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  // 获取点击到的车轮
  let intersects = raycaster.intersectObjects(wheels, true);
  console.log(intersects, 'intersects>>>>>>>>>>>>>>>>>>', raycaster, mouse);
  //当intersects.length > 0说明碰撞到物体,获取最近的物体名称进行判断
  if (intersects.length > 0) {
    // 点击到的车轮
    const intersect = intersects[ 0 ];
    // handleAnimation();
    // console.log(intersect.material.fog);
    // ...添加你需要的事件,如弹窗信息等
  }
}


function addEventToWheel() {
  let content = document.getElementById('webgl');
  content.addEventListener("click",
    onClickMesh,
  false)
}

最终效果

没有安装录屏软件,浅浅的截个图吧

总结

浅浅的体验了一下 Three.js,但是很多个性化需求,还是要根据官方文档还有官方举例结合来实现。而且如果要很好看很酷炫的效果,就得结合建模了...

后面还听说了 Babylon.js ,两者都是 WebGL框架,但是差异并没有特别大,我也暂时没有去深入了解,只觉得前端技术博大精深,好头大......

稍微记录一下,欢迎各位大佬交流指导!!!

另外,我一般喜欢在语雀记录,这是刚从语雀挪过来的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴能和你聊聊three.js初体验three.js是一个用于创建和展示3D图形的开源JavaScript库。它提供了丰富的功能和工具,使开发者能够在Web浏览器中轻松地创建交互式的3D场景。 首先,你需要在你的HTML页面中引入three.js库。你可以通过下载three.js文件并将其添加到你的项目中,或者使用CDN链接。接下来,你需要一个HTML元素作为3D场景的容器,例如一个div元素。给这个元素一个唯一的id,以便在JavaScript代码中引用它。 接下来,你可以开始编写JavaScript代码来创建3D场景。首先,你需要创建一个场景对象,使用`new THREE.Scene()`来实现。然后,你可以创建相机对象,例如透视相机`new THREE.PerspectiveCamera()`,并设置它的位置和方向。 接着,你需要创建渲染器对象,并将其添加到页面中。使用`new THREE.WebGLRenderer()`来创建渲染器对象,并设置它的大小和背景色。然后,使用`renderer.domElement`将渲染器对象添加到之前创建的HTML元素中。 接下来,你可以开始创建并添加3D对象到场景中。例如,你可以创建一个立方体对象`new THREE.Mesh()`,并设置其材质和位置。然后,使用`scene.add()`方法将这个立方体对象添加到之前创建的场景中。 最后,你需要编写渲染循环函数,并在每一帧中更新场景和渲染器。使用`requestAnimationFrame()`来实现渲染循环,并在循环中更新相机位置、物体的位置或其他动画效果。 通过这些基本步骤,你就可以初步体验three.js并创建简单的3D场景了。当然,这只是three.js的一个简单入门示例,你可以进一步探索three.js的文档和示例来学习更多高级功能和技巧。祝你玩得开心!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值