转载链接:
https://zhuanlan.zhihu.com/p/333615381
https://techbrood.com/threejs/docs
模型下载链接:
https://sketchfab.com/feed
属于对该项目的学习,对于其中基础部分的细分和笔记
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)。下面这张图可以帮助你理解:
这几个参数所限定的绿色3D空间被称之为视椎体(View Frustum),用来裁剪视图,在该视锥体以外的物体将不会被渲染。我们暂时可以先不管,但你需要了解这个空间和渲染性能有关。
两张图结合看,可以知道fov是存在纵向和横向的,就是有y方向和x方向的
当然,从第一张图可以知道,你需要将相机移动位置,不然就会和物体重叠,导致拍不到了。
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
//设置相机的摆放位置
camera.position.z = 10;
上面说过为啥要移动相机位置了。
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 。
萨勒芬妮。下载好后,解压,放进项目文件的static目录(自己选,我一般是放在这里)。
声明一个加载器,加载我们下载的模型,并把它添加到场景中,在animate函数上面添加代码:
const gltfLoader = new GLTFLoader()
gltfLoader.load('/static/seraphine/scene.gltf', (gltf) => {
var model = gltf.scene
scene.add(model)
})
刷新页面,场景里有了模糊的黑色的小人,这是因为我们还没有给她添加纹理。
来给她上个色:
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);
});
好,现在写几点注意事项
好了,现在刷新下,她变成这样了,可以看到已经有颜色了,但还是很糊:
原因是设备的物理像素分辨率与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是什么意思的往上翻摄像机部分。
此时模型:
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()
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提供的光源有很多种,有的可以产生阴影(平行光,点光源等),有的不行(半球光等):
先添加个平行光:
对于平行光的解释:
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)
再次感谢,康康卷毛。写的很详细,学到很多。
链接: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>