three.js和vue.js学习 -- 创建一个3d模型demo

写在开头

转载链接:
https://zhuanlan.zhihu.com/p/333615381
https://techbrood.com/threejs/docs
模型下载链接:
https://sketchfab.com/feed
属于对该项目的学习,对于其中基础部分的细分和笔记

准备

three的流程参考图
在这里插入图片描述

Part1:创建项目,引入three

同样使用vue-cli脚手架直接搭建vue项目
看到这,不了解为什么要用vue-cli构建项目的同学是不是想直接关闭了?
解释:http://www.yanhuangxueyuan.com/three.js_course/longword/crossdomain.html

不需要加载外部贴图和模型文件的three.js案例,可以直接使用浏览器打开.html案例文件,通常一个threejs项目案例往往都会加载一些外部模型,因此打开threejs案例要搭建一个本地的静态服务器,否则的话,threejs案例无法正常打开,浏览器控制台会提示跨域问题。
通过Three.js加载obj、FBX等格式外部模型文件的时候是ajax异步加载数据的过程,需要建立本地服务器来解决,如果不这样直接使用浏览器打开加载三维模型的.html文件,会出现报错无法模型文件无法加载,浏览器控制报错跨域问题的情况。
浏览器控制台报错:
three.js:30833 Access to XMLHttpRequest at ‘file://…’ from origin ‘null’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

跨域的原因就是浏览器不支持file协议,只支持http等协议,所以,要想解决跨域问题,需要起服务器,不管你是前端或是后端服务器都行。也可以使用npm起一个live-server都可以。
因此,在这里借用vue-cli的方便之处,为了学习vue的语法特性等,就直接用vue了。
不用vue,webpack也一样,但是要起服务器,要自己管理安装包等。

cd 到当前项目目录下
vue init webpack xxxxx
xxx表示项目名称

安装 three

npm install -D three

引入three

import * as THREE from 'three'

Part2: 创建容器

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

Part3:创建场景

//1.创建场景
const scene = new THREE.Scene();
//设置场景的颜色
scene.background = new THREE.Color('#eee');

也可以通过自定义的纹理来设置背景贴图

Part4:创建渲染器

//2.创建渲染器
//获取canvas
const canvas = document.querySelector('#three');
//创建一个WebGLRenderer
const renderer = new THREE.WebGLRenderer({
 canvas,
 antialias:true //是否执行抗锯齿
});

Part5:创建相机

知识点:关于相机
Three.js的架构支持多种camera,这里使用最常见的远景相机(PerspectiveCamera),也就是类似于人眼观察的方式。第一个属性75设置的是视角(field of view)。
第二个属性设置的是相机拍摄面的长宽比(aspect ratio)。我们几乎总是会使用元素的宽除以高,否则会出现挤压变形。
接下来的2个属性是近裁剪面(near clipping plane) 和 远裁剪面(far clipping plane)。下面这张图可以帮助你理解:
three.js和vue.js学习 -- 创建一个3d模型demo_第1张图片
这几个参数所限定的绿色3D空间被称之为视椎体(View Frustum),用来裁剪视图,在该视锥体以外的物体将不会被渲染。我们暂时可以先不管,但你需要了解这个空间和渲染性能有关。

两张图结合看,可以知道fov是存在纵向和横向的,就是有y方向和x方向的
three.js和vue.js学习 -- 创建一个3d模型demo_第2张图片
当然,从第一张图可以知道,你需要将相机移动位置,不然就会和物体重叠,导致拍不到了。

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
//设置相机的摆放位置
camera.position.z = 10;

上面说过为啥要移动相机位置了。

  • fov – 摄像机视锥体垂直视野角度 比作人的视野广度,想象你看正前方的角度,为50,为180时候的情况,自然角度适当看东西才聚集
  • aspect – 摄像机看锥体的横纵比, 一般为屏幕宽高比
  • near – 渲染的最近距离,超过不渲染
  • far – 渲染的最远距离,超过不渲染 所以过大会影响性能

Part6:创建动画循环函数
这步流程图中没有?图是死的,人是活的。
先将循环函数写出来,后边一点一点往函数里添加内容,才能看出所做的成果。

let animate = function (){
        renderer.render(scene, camera); //添加场景和相机,没啥好解释的
        requestAnimationFrame(animate);
      }

这将创建一个循环,以每秒60次的频率来绘制场景。
这边说一下requestAnimationFrame这个函数:
requestAnimationFrame这个函数,它用来替代 setInterval, 这个新接口具备多个优点,比如浏览器Tab切换后停止渲染以节约资源、和屏幕刷新同步避免无效刷新、在不支持该接口的浏览器中能安全回退为setInterval。
额。。反正我也新手,不懂setInterval这个函数怎么用,不过应该是个定时器,也就是设置帧率的?不清楚,感兴趣可以查查

代码雏形

此时界面显示的是灰色的

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

<script>
import * as THREE from 'three'

export default {
  name: 'temp',
  methods:{
    initThree(){
      //1.创建场景
      const scene = new THREE.Scene();
      //设置场景的颜色
      scene.background = new THREE.Color('#eee');

       //2.创建渲染器
      //获取canvas
      const canvas = document.querySelector('#three');
      //创建一个WebGLRenderer
      const renderer = new THREE.WebGLRenderer({
        canvas,
        antialias:true //是否执行抗锯齿
      });

      //3.创建相机
      //透视相机PerspectiveCamera,比较常用,且模拟人眼看到的景象
      const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);


      //设置相机的摆放位置
      camera.position.z = 10;

      //同时,需要一个动画循环函数,Three.js的每一帧都会执行这个函数。
      let animate = function (){
        renderer.render(scene, camera);
        requestAnimationFrame(animate);
      }

    }
  }
}

</script>

<style scoped>

</style>

Part7:引入3D模型

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

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

这里需要说一下,没有用webpack包管理工具下载three的话,需要在官网下载-master的文件,在那个文件引入,这也是看似没有用到多少vue的地方,我仍然选择vue-cli脚手架创建项目的原因,还有其他原因后面说

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

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

three.js和vue.js学习 -- 创建一个3d模型demo_第3张图片

萨勒芬妮。下载好后,解压,放进项目文件的static目录(自己选,我一般是放在这里)。

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

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

刷新页面,场景里有了模糊的黑色的小人,这是因为我们还没有给她添加纹理。
three.js和vue.js学习 -- 创建一个3d模型demo_第4张图片
来给她上个色:

gltfLoader.load('static/seraphine/scene.gltf', (gltf) => {
        let model = gltf.scene;

        //遍历模型的每一部分
        //traverse这个方法可以遍历调用者和调用者的所有后代
        //所以这里的o就是模型的每一部分
        //注意,我们这里是让他自己为我们找到后代中的Mesh并渲染对应的皮肤
        model.traverse((o) => {
          //将图片作为纹理加载
          let explosionTexture = new THREE.TextureLoader().load(
            'static/seraphine/textures/Mat_cwfyfr1_userboy17.bmp_diffuse.png'
          );
          //调整纹理方向,默认为真。翻转图像的Y轴以匹配WebGL纹理坐标空间。
          //此处不需要反转,当然也可以试试反转以后什么效果
          explosionTexture.flipY = false;
          //将纹理图生成基础网格材质(meshBasicMaterial)
          const material = new THREE.MeshBasicMaterial({
            map: explosionTexture
          });
          //给模型每部分上材质
          o.material = material;

        });
        scene.add(model);
      });

好,现在写几点注意事项

  • gltf文件一个json文件格式,用来快速递交和加载3D内容。
  • 那么就好理解了,这里我接触js不算很久,就详细给自己解释一下
  • 调用load函数,返回一个对象object,并调用你写的回调函数
  • 这个对象中有scene, cameras animations(动画效果)
  • 回调函数的参数是这个对象,名叫gltf,这个回调函数的作用就是将gltf中的scene加入到你自己定义的场景scene中,并且遍历每个部分为模型上色。

好了,现在刷新下,她变成这样了,可以看到已经有颜色了,但还是很糊:
three.js和vue.js学习 -- 创建一个3d模型demo_第5张图片
原因是设备的物理像素分辨率与CSS像素分辨率的比值的问题,我们的canvas绘制出来后图片因为高清屏设备的影响,导致图片变大,然而我们在浏览器的渲染窗口并没有变大,因此图片会挤压缩放使得canvas画布会变得模糊。

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

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

//函数:重新设置渲染器的展示大小
      let resizeRendererToDisplaySize = function (renderer){
        //这里没看明白往上翻
        const canvas = renderer.domElement;
        let width = window.innerWidth;
        let height = window.innerHeight;
        //判断css像素分辨率就和物理像素分辨率是否统一
        let canvasPixelWidth = canvas.width / window.devicePixelRatio;
        let canvasPixelHeight = canvas.height / window.devicePixelRatio;
        //判断是否需要调整
        const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
        if (needResize){
          renderer.setSize(width, height, false);
        }
        return needResize;
      }

在animate函数内调用它:

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

        //判断渲染器是否调整,若调整,相机也需要调整aspect
        if (resizeRendererToDisplaySize(renderer)){
          const canvas = renderer.domElement;
          //重新设置摄像机看视锥体的横纵比,横纵比一般为屏幕宽高比,不然会挤压变形
          camera.aspect = canvas.clientWidth / canvas.clientHeight;
          camera.updateProjectionMatrix();
        }
      }

此处忘记camera.aspect是什么意思的往上翻摄像机部分。
此时模型:
three.js和vue.js学习 -- 创建一个3d模型demo_第6张图片

Part:8 添加轨道控制器

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

引入轨道控制器:

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()

现在就可以拉近点看脸了:
three.js和vue.js学习 -- 创建一个3d模型demo_第7张图片

Part 9:添加光与影
还是先加个地板叭,不然影子没地方投。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(这里是地板),并添加到场景中:

et floor = new THREE.Mesh(floorGeometry, floorMaterial);
      //为什么要转?因为背景是垂直地面的,不转就不是地板了。
      //以及是沿着x轴转90度,就到地面了
      floor.rotation.x = -0.5 * Math.PI;
      floor.receiveShadow = true;
      //让地板离人物有一段距离,产生地板的效果
      floor.position.y = -0.001;
      scene.add(floor);

再给他调整下位置,让他水平放置在萨勒芬妮的脚下,并让地板可以接收投影。
three.js和vue.js学习 -- 创建一个3d模型demo_第8张图片
可是现在是黑的,跟我们加的颜色不一样,那是因为没有光,Three.js提供的光源有很多种,有的可以产生阴影(平行光,点光源等),有的不行(半球光等):

先添加个平行光:
对于平行光的解释:

  • 影响使用 兰伯特网孔材料(MeshLambertMaterial) 或 Phong网孔材料(MeshPhongMaterial) 的对象。
  • hex – 光源颜色的RGB数值。
  • intensity – 光源强度的数值。
  • 创建一个光照,从一个特定的方向,而不是从一个特定的位置。这个光看起来就像光源位于无限远处,因此它产生的光线都是平行的。 最好的类比是一个像太阳一样的光源:太阳是如此遥远,所有的阳光照射到物体上都几乎来自同一个角度。
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
      //可以产生阴影
      dirLight.castShadow = true;
      dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
      scene.add(dirLight);

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

hex - (可选参数) 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)

three.js和vue.js学习 -- 创建一个3d模型demo_第9张图片
大功告成。

结语

再次感谢,康康卷毛。写的很详细,学到很多。
链接:https://zhuanlan.zhihu.com/p/333615381

完整代码

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

</template>

<script>
import * as THREE from 'three'
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
// 轨道控制器
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'

export default {
  name: 'beautifulGirl',
  methods: {
    initThree() {
    initThree(){
      //1.创建场景
      const scene = new THREE.Scene();
      //设置场景的颜色
      scene.background = new THREE.Color('#eee');
      //为场景加上雾的效果
      scene.fog = new THREE.Fog('#eee', 20, 100)

       //2.创建渲染器
      //获取canvas
      const canvas = document.querySelector('#three');
      //创建一个WebGLRenderer
      const renderer = new THREE.WebGLRenderer({
        canvas,
        antialias:true //是否执行抗锯齿
      });
      renderer.shadowMap.enabled = true;

      //3.创建相机
      //透视相机PerspectiveCamera,比较常用,且模拟人眼看到的景象
      const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
      //设置相机的摆放位置
      camera.position.z = 10;

      //4.加载3D模型
      //gltf文件一个json文件格式,用来快速递交和加载3D内容。
      //那么就好理解了,这里我接触js不算很久,就详细给自己解释一下
      //调用load函数,返回一个对象object,并调用你写的回调函数
      //这个对象中有scene, cameras animations(动画效果)
      //回调函数的参数是这个对象,起名叫gltf,这个回调函数的作用就是将gltf中的scene加入到你自己定义的场景scene中。
      const gltfLoader = new GLTFLoader();
      gltfLoader.load('/static/seraphine/scene.gltf', (gltf) => {
        let model = gltf.scene;

        //遍历模型的每一部分
        //traverse这个方法可以遍历调用者和调用者的所有后代
        //所以这里的o就是模型的每一部分
        model.traverse((o) => {
          //将图片作为纹理加载
          let explosionTexture = new THREE.TextureLoader().load(
            'static/seraphine/textures/Mat_cwfyfr1_userboy17.bmp_diffuse.png'
          );
          //调整纹理方向,默认为真。翻转图像的Y轴以匹配WebGL纹理坐标空间。
          //此处不需要反转,当然也可以试试反转以后什么效果
          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);
      });

      //此时图片很模糊,因为物理像素分辨率和css像素分辨率的比值问题
      //函数:重新设置渲染器的展示大小
      let resizeRendererToDisplaySize = function (renderer){
        //这里没看明白往上翻
        const canvas = renderer.domElement;
        let width = window.innerWidth;
        let height = window.innerHeight;
        //判断css像素分辨率就和物理像素分辨率是否统一
        let canvasPixelWidth = canvas.width / window.devicePixelRatio;
        let canvasPixelHeight = canvas.height / window.devicePixelRatio;
        //判断是否需要调整
        const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
        if (needResize){
          renderer.setSize(width, height, false);
        }
        return needResize;
      }

      //5. 引入轨道控制器
      const controls = new OrbitControls(camera, renderer.domElement);
      //加点阻尼感。更真实
      controls.enableDamping = true;

      //6.添加光与影
      //Three.js里的物体(一般叫网格Mesh)由两部分构成,一是它的形状,二是它的材质
      let floorGeometry = new THREE.PlaneGeometry(3000, 3000);
      //Phong网格材质,是一种用于具有镜面高光的光泽表面的材质
      let floorMaterial = new THREE.MeshPhongMaterial({color:0x9370DB});
      //生成Mesh(在这里是地板),并添加到场景中
      let floor = new THREE.Mesh(floorGeometry, floorMaterial);
      //为什么要转?因为背景是垂直地面的,不转就不是地板了。
      //以及是沿着x轴转90度,就到地面了
      floor.rotation.x = -0.5 * Math.PI;
      floor.receiveShadow = true;
      //让地板离人物有一段距离,产生地板的效果
      floor.position.y = -0.001;
      scene.add(floor);

      //添加光源
      //平行光
      //影响使用 兰伯特网孔材料(MeshLambertMaterial) 或 Phong网孔材料(MeshPhongMaterial) 的对象。
      //hex -- 光源颜色的RGB数值。
      //intensity -- 光源强度的数值。
      //创建一个光照,从一个特定的方向,而不是从一个特定的位置。这个光看起来就像光源位于无限远处,因此它产生的光线都是平行的。 最好的类比是一个像太阳一样的光源:太阳是如此遥远,所有的阳光照射到物体上都几乎来自同一个角度。
      const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
      //可以产生阴影
      dirLight.castShadow = true;
      dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
      scene.add(dirLight);

      //添加半球光让场景更加真实
      const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
      hemLight.position.set(0, 48, 0);
      scene.add(hemLight);


      //同时,需要一个动画循环函数,Three.js的每一帧都会执行这个函数。
      let animate = function (){
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(animate);

        //判断渲染器是否调整,若调整,相机也需要调整aspect
        if (resizeRendererToDisplaySize(renderer)){
          const canvas = renderer.domElement;
          //重新设置摄像机看视锥体的横纵比,横纵比一般为屏幕宽高比,不然会挤压变形
          camera.aspect = canvas.clientWidth / canvas.clientHeight;
          camera.updateProjectionMatrix();
        }
      }

    }
   }
  },
  mounted() {
    this.initThree();
  }
}

</script>

<style scoped>
#three {
  width: 100%;
  height: 100%;
  position: fixed;
  left: 0;
  top: 0;
}
</style>

你可能感兴趣的:(Three.js,three.js)