添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
有腹肌的黄花菜  ·  Building wheel for ...·  4 月前    · 
刚毅的火车  ·  TypeScript ...·  1 年前    · 
儒雅的豌豆  ·  Visual Studio 中的 Git ...·  1 年前    · 
Vue+Three.js,新手demo

Vue+Three.js,新手demo

准备

我们需要建立一个vue项目,这里我直接用vue-cli脚手架了。

Part 1:引入three.js

项目文件夹里打开终端窗口,并运行:

npm install --save three

在需要使用three.js的组件内引入

import * as THREE from 'three'

Part 2:创建容器

创建canvas标签,为3D渲染建立容器。

<template>
    <canvas id="three"></canvas>
  </div>
</template>

Part 3:创建场景

Three.js依赖一些要素,第一是scene,第二是render,第三是carmea。

我们首先为Three.js创建一个scene:

const scene = new THREE.Scene()

scene可以理解为我们将要渲染的环境、背景。我们可以给它换个颜色:

scene.background = new THREE.Color('#eee')

也可以通过自定义的 纹理(Texture) 来为场景设置背景贴图。


接下来我们拿到canvas:

const canvas = document.querySelector('#three')

创建一个WebGLRenderer,将canvas和 配置参数 传入:

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })


最后我们来创建一个camera用来观看场景里的内容,Three.js提供多种相机,比较常用的是PerspectiveCamera(透视摄像机)以及OrthographicCamera (正交投影摄像机)。

透视相机用来模拟人眼所看到的景象,物体的大小会受远近距离的影响,它是3D场景的渲染中使用得最普遍的投影模式。

而正交投影摄像机,不具有透视效果,即物体的大小不受远近距离的影响。

这里我们使用PerspectiveCamera(透视摄像机):

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000)

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )具有四个参数:

  • fov — 摄像机视锥体垂直视野角度。可以理解为人类的视野广度。
  • aspect — 摄像机视锥体横纵比。渲染结果的横向尺寸和纵向尺寸的比值,这里我们使用的是 浏览器窗口的宽高比。
  • near — 摄像机视锥体近端面。一切比近面更近的事物将不被渲染。
  • far — 摄像机视锥体远端面。一切比远面更远的事物将不被渲染,但是设置过大可能会影响性能。

    这些参数一起定义了摄像机的视锥体。
PerspectiveCamera(透视摄像机)

生成的camera默认是放在中心点(0,0,0)的,但这是待会模型要放的位置,因此,我们把摄像机挪个位置:

camera.position.z = 10


Three.js 需要一个动画循环函数,Three.js 的每一帧都会执行这个函数。

function animate() {
  renderer.render(scene, camera)
  requestAnimationFrame(animate)
animate()


我们的场景的准备工作已经做好了,现在是一片灰色,没有物体。vue组件代码是这样的:

<template>
    <canvas id="three"></canvas>
</template>
<script>
import * as THREE from 'three'
export default {
  mounted() {
    this.initThree()
  methods: {
    initThree() {
      const scene = new THREE.Scene()
      scene.background = new THREE.Color('#eee')
      const canvas = document.querySelector('#three')
      const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
      const camera = new THREE.PerspectiveCamera(
        window.innerWidth / window.innerHeight,
        0.1,
      camera.position.z = 10
      function animate() {
        renderer.render(scene, camera)
        requestAnimationFrame(animate)
      animate()
</script>
<style scoped>
#three {
  width: 100%;
  height: 100%;
  position: fixed;
  left: 0;
  top: 0;
</style>

Part 4 引入3D模型

官网介绍说three.js的核心专注于3D引擎最重要的组件。其它很多有用的组件 - 如控制器(control)、加载器(loader)以及后期处理效果(post-processing effect)这些就需要我们单独去引入。

如果我们想要加载外部3D模型,那么就需要用到加载器(loader),而Three.js提供的加载器又有好多种类型,分别可以加载不同的文件格式,其中官方比较推荐的是glTF格式,那么我们这里就使用glTF加载器:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

当然你也可以选择FBX、OBJ或者COLLADA等其他加载器,如果你是npm安装的three.js,可以去node_modules/three/examples/jsm/loaders 目录下查看更多支持的加载器。


加载器有了,现在我们还没有模型。可以去搜索一些免费的3D模型素材下载,当然你也可以自己做,我是去 Sketchfab 下载的,如果对3D模型的制作感兴趣也可以学习一下,推荐一个免费的软件 Blender

萨勒芬妮。下载好后,解压,放进项目文件的public目录。

声明一个加载器,加载我们下载的模型,并把它添加到场景中,在animate函数上面添加代码:

    const gltfLoader = new GLTFLoader()
      gltfLoader.load('/seraphine/scene.gltf', (gltf) => {
        var model = gltf.scene
        scene.add(model)

刷新页面,场景里有了模糊的黑色的小人,这是因为我们还没有给她添加纹理。

来给她上个色:

      gltfLoader.load('/seraphine/scene.gltf', (gltf) => {
        let model = gltf.scene
        //添加这段代码
        //遍历模型每部分
        model.traverse((o) => {
          //将图片作为纹理加载
          let explosionTexture = new THREE.TextureLoader().load(
            '/seraphine/textures/Mat_cwfyfr1_userboy17.bmp_diffuse.png'
          //调整纹理图的方向
          explosionTexture.flipY = false
          //将纹理图生成基础网格材质(MeshBasicMaterial)
          const material = new THREE.MeshBasicMaterial({
            map: explosionTexture,
          //给模型每部分上材质
          o.material = material
        scene.add(model)

好了,现在刷新下,她变成这样了,可以看到已经有颜色了,但还是很糊:

原因是设备的物理像素分辨率与CSS像素分辨率的比值的问题,我们的canvas绘制出来后图片因为高清屏设备的影响,导致图片变大,然而我们在浏览器的渲染窗口并没有变大,因此图片会挤压缩放使得canvas画布会变得模糊。

修改它我们要用到devicePixelRatio这个属性,MDN解释:

此属性返回当前显示设备的物理像素分辨率与CSS像素分辨率的比值。该值也可以被解释为像素大小的比例:即一个CSS像素的大小相对于一个物理像素的大小的比值。

添加函数:

     function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement
        var width = window.innerWidth
        var height = window.innerHeight
        var canvasPixelWidth = canvas.width / window.devicePixelRatio
        var canvasPixelHeight = canvas.height / window.devicePixelRatio
        const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height
        if (needResize) {
          renderer.setSize(width, height, false)
        return needResize

在animate函数内调用它:

      function animate() {
        controls.update()
        renderer.render(scene, camera)
        requestAnimationFrame(animate)
        //添加下面代码
        if (resizeRendererToDisplaySize(renderer)) {
          const canvas = renderer.domElement
          camera.aspect = canvas.clientWidth / canvas.clientHeight
          camera.updateProjectionMatrix()

模型看起来好多了。

Part 5 添加轨道控制器

现在嘛,就只能看到萨勒芬妮的侧脸,我想康康正脸怎么办。

引入轨道控制器:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

创建它,可以写在animate函数上面:

const controls = new OrbitControls(camera, renderer.domElement)

给它加点阻尼感,更真实点,泥可以对比下加不加的区别:

controls.enableDamping = true

最后在animate函数里调用它,要写在 renderer.render(scene, camera)前面啊:

controls.update()

现在就可以拉近点看脸了:

Part 6 添加光与影

还是先加个地板叭,不然影子没地方投。Three.js里物体(一般叫网格Mesh)由两部分构成,一是它的形状,二是它的材质,我们给地板创建它们:

let floorGeometry = new THREE.PlaneGeometry(3000, 3000)
let floorMaterial = new THREE.MeshPhongMaterial({color: 0xff0000})

平面几何体,PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)

width — 平面沿着X轴的宽度。默认值是1。
height — 平面沿着Y轴的高度。默认值是1。
widthSegments — (可选)平面的宽度分段数,默认值是1。
heightSegments — (可选)平面的高度分段数,默认值是1。

Phong网格材质(MeshPhongMaterial)

是一种用于具有镜面高光的光泽表面的材质。

生成Mesh,并添加到场景中:

let floor = new THREE.Mesh(floorGeometry, floorMaterial)
floor.rotation.x = -0.5 * Math.PI
floor.receiveShadow = true
floor.position.y = -0.001
scene.add(floor)

再给他调整下位置,让他水平放置在萨勒芬妮的脚下,并让地板可以接收投影。

可是现在是黑的,跟我们加的颜色不一样,那是因为没有光,Three.js提供的光源有很多种,有的可以产生阴影(平行光,点光源等),有的不行(半球光等):

先添加个平行光:

      const dirLight = new THREE.DirectionalLight(0xffffff, 0.6)
      //光源等位置
      dirLight.position.set(-10, 8, -5)
      //可以产生阴影
      dirLight.castShadow = true
      dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024)
      scene.add(dirLight)

平行光一般用来模拟太阳光,DirectionalLight( color : Integer, intensity : Float )

color - (可选参数) 16进制表示光的颜色。 缺省值为 0xffffff (白色)。
intensity - (可选参数) 光照的强度。缺省值为1。

现在地板有颜色了,还可以添加多个光源,让场景看起来更真实:

      const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6)
      hemLight.position.set(0, 48, 0)
      scene.add(hemLight)

半球光光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )

skyColor - (可选参数) 天空中发出光线的颜色。 缺省值 0xffffff。
groundColor - (可选参数) 地面发出光线的颜色。 缺省值 0xffffff。
intensity - (可选参数) 光照强度。 缺省值 1。

想要产生影子,还需要在renderer下添加:

      const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
      //加这句
      renderer.shadowMap.enabled = true;

以及:

        model.traverse((o) => {
          //将图片作为纹理加载
          let explosionTexture = new THREE.TextureLoader().load(
            '/seraphine/textures/Mat_cwfyfr1_userboy17.bmp_diffuse.png'
          //调整纹理图的方向
          explosionTexture.flipY = false
          //将纹理图生成基础网格材质(MeshBasicMaterial)
          const material = new THREE.MeshBasicMaterial({
            map: explosionTexture,
          //给模型每部分上材质
          o.material = material
          //加这句,让模型等每个部分都能产生阴影
          if (o.isMesh) {
            o.castShadow = true
            o.receiveShadow = true

现在就有影子啦!

最后最后最后,给场景加个雾叭:

const scene = new THREE.Scene()
scene.background = new THREE.Color('#eee')
//在代码上面声明场景等下面加这句:
scene.fog = new THREE.Fog('#eee', 20, 100)


OK,完事。


喔喔代码忘放了:

<template>
    <canvas id="three"></canvas>
</template>
<script>
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default {
  mounted() {
    this.initThree()
  methods: {
    initThree() {
      const scene = new THREE.Scene()
      scene.background = new THREE.Color('#eee')
      scene.fog = new THREE.Fog('#eee', 20, 100)
      const canvas = document.querySelector('#three')
      const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
      renderer.shadowMap.enabled = true
      const camera = new THREE.PerspectiveCamera(
        window.innerWidth / window.innerHeight,
        0.1,
      camera.position.z = 10
      const gltfLoader = new GLTFLoader()
      gltfLoader.load('/seraphine/scene.gltf', (gltf) => {
        let model = gltf.scene
        //遍历模型每部分
        model.traverse((o) => {
          //将图片作为纹理加载
          let explosionTexture = new THREE.TextureLoader().load(
            '/seraphine/textures/Mat_cwfyfr1_userboy17.bmp_diffuse.png'
          //调整纹理图的方向
          explosionTexture.flipY = false
          //将纹理图生成基础网格材质(MeshBasicMaterial)
          const material = new THREE.MeshBasicMaterial({
            map: explosionTexture,
          //给模型每部分上材质
          o.material = material
          if (o.isMesh) {
            o.castShadow = true
            o.receiveShadow = true
        scene.add(model)
      const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6)
      hemLight.position.set(0, 48, 0)
      scene.add(hemLight)
      const dirLight = new THREE.DirectionalLight(0xffffff, 0.6)
      //光源等位置
      dirLight.position.set(-10, 8, -5)
      //可以产生阴影
      dirLight.castShadow = true
      dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024)
      scene.add(dirLight)
      let floorGeometry = new THREE.PlaneGeometry(8000, 8000)
      let floorMaterial = new THREE.MeshPhongMaterial({
        color: 0x857ebb,
        shininess: 0,
      let floor = new THREE.Mesh(floorGeometry, floorMaterial)
      floor.rotation.x = -0.5 * Math.PI
      floor.receiveShadow = true
      floor.position.y = -0.001
      scene.add(floor)
      const controls = new OrbitControls(camera, renderer.domElement)
      controls.enableDamping = true
      function animate() {
        controls.update()
        renderer.render(scene, camera)
        requestAnimationFrame(animate)
        if (resizeRendererToDisplaySize(renderer)) {
          const canvas = renderer.domElement
          camera.aspect = canvas.clientWidth / canvas.clientHeight
          camera.updateProjectionMatrix()
      animate()
      function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement
        var width = window.innerWidth
        var height = window.innerHeight
        var canvasPixelWidth = canvas.width / window.devicePixelRatio
        var canvasPixelHeight = canvas.height / window.devicePixelRatio
        const needResize =
          canvasPixelWidth !== width || canvasPixelHeight !== height
        if (needResize) {
          renderer.setSize(width, height, false)
        return needResize
</script>
<style scoped>
#three {
  width: 100%;
  height: 100%;