个人项目上线
使用vercel
部署:https://vercel.com/
- 本地全局安装
npm install vercel -g
- 打包项目
npm run build
- 配置部署命令:
"scripts": {
"dev": "vite",
"build": "vite build",
"deploy": "vercel --prod"
},
- 执行
vercel login
使用github
登录vercel
- 执行
npm run deploy
即可:
灯光
AmbientLight
环境光源会无差别地作用在物体的各个方向,不会造成阴影等效果
添加光照前:
添加光照后:
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
scene.add(ambientLight);
DirectionalLight
平行光是从单一方向照射而来的光,默认平行光来源为正上方:(0,1,0)的位置
const directionalLight = new THREE.DirectionalLight("#ff0", 0.5);
scene.add(directionalLight);
平行光默认照向的位置是原点
HemisphereLight半球光
半球光自带两个方向,分为skyColor
和groudColor
,光的默认位置也是(0,1,0)
const hemisphereLight = new THREE.HemisphereLight("#ff0", "#00f", 1);
scene.add(new THREE.HemisphereLightHelper(hemisphereLight, 5));
scene.add(hemisphereLight);
注意看,半球光在光交界处的颜色是渐变的,即两种光深浅的交融
PointLight
点光源就是从某个点发出的光,默认从(0,0,0)的位置发出:
const pointLight = new THREE.PointLight("#f0f", 50);
scene.add(pointLight);
const pointLightHelper = new THREE.PointLightHelper(pointLight, 1);
scene.add(pointLightHelper);
通过distance
属性可以配置光照射的距离,默认是0,代表无限远
如果将光移动到某个位置,distance
设为达不到物体的距离,那么光就不会作用到distance
范围外的物体,比如将光移动到下面这个位置并且设置光的距离:
decay
对应的是光的衰减值,通常情况下使用的默认的就行,默认值是2,2已经是threejs
算出来的最符合物理规律的值了
RectAreaLight
平面光光源,类似生活中摄影棚里的灯:
const rectLight = new THREE.RectAreaLight(0xffffff, 2, 3, 1);
const rectLightFolder = gui.addFolder("rectLight");
rectLightFolder.add(rectLight.position, "x").min(-10).max(10).onChange(() => {
rectLight.lookAt(new THREE.Vector3(0,0,0))
});
rectLightFolder.add(rectLight.position, "y").min(-10).max(10).onChange(() => {
rectLight.lookAt(new THREE.Vector3(0,0,0))
});
rectLightFolder.add(rectLight.position, "z").min(-10).max(10).onChange(() => {
rectLight.lookAt(new THREE.Vector3(0,0,0))
});
rectLightFolder.add(rectLight, "width").min(-10).max(10).onChange(() => {
rectLight.lookAt(new THREE.Vector3(0,0,0))
});
rectLightFolder.add(rectLight, "height").min(-10).max(10).onChange(() => {
rectLight.lookAt(new THREE.Vector3(0,0,0))
});
scene.add(rectLight);
const rectLightHelper = new RectAreaLightHelper(rectLight);
scene.add(rectLightHelper);
代码中设置了在每次修改光源位置、宽高时都让光源重新照向(lookAt
)原点,如果不这么设置,光源将保持初次声明的方向不变
注意,这个光源只能作用于MeshStandardMaterial
和MeshPhysicalMaterial
两种材质。
SpotLight
聚光灯,照射方式类似手电筒,
const spotLight = new THREE.SpotLight("#F0F");
const spotLightFolder = gui.addFolder("spotLight");
spotLightFolder.add(spotLight.position, "x").min(-10).max(10);
spotLightFolder.add(spotLight.position, "y").min(-10).max(10);
spotLightFolder.add(spotLight.position, "z").min(-10).max(10);
spotLightFolder.add(spotLight, "intensity").min(-10).max(10);
spotLightFolder.add(spotLight, "distance").min(0).max(10);
spotLight.angle = Math.PI / 4;
scene.add(spotLight);
const spotLightHelper = new THREE.SpotLightHelper(spotLight);
scene.add(spotLightHelper);
另外,通过penumbra
属性,可以配置出灯光在边缘区域的衰减程度:
另外,聚光灯默认始终看向原点,如果想要改变其照射的方向,使用lookAt
是无效的,必须通过修改spotLight.target.position
才能实现:
scene.add(spotLight.target);
spotLight.target.position.set(-1, 1, 0);
性能问题
灯光是很耗性能的,我们应该使用尽可能少的光源实现,光的性能消耗从高到低排行如下:
T0:SpotLight、RectAreaLight(最耗性能)
T1:DirectionalLight 、PointLight
T2:AmbientLight 、HemisphereLight(最不耗性能)
所以,当来到性能优化层面时,灯光的实现可以考虑在一开始进行3D建模时就给纹理附加上灯光,而不是使用threejs
里的光源实现,不过这样的问题是做不到实时的灯光移动,对于需要动态变化的场景可能就略有欠缺
比如threejs-journey.com
上提供的示例:
这个场景内的几何体,都是使用原本就有灯光效果的纹理实现的
阴影
阴影在threejs
中是被计算成一张阴影贴图来展示的,比如可以看官方给的expample
示例:
画面中存在一个从上往下照射的directionalLight
,那么经过threejs
计算后,得到左上角第一张的阴影贴图;
画面中存在一个照向左下角的spotLight
,这个spotLight
只照得到环状结几何体,所以左上角第二张阴影贴图只有这个环状结
这就是threejs
内部处理阴影的原理
阴影的一般实现方式
- 几何体网格设置
castShadow = true
- 某个需要展示投影的平面
Mesh
设置receiveShadow = true
- 灯光设置
castShadow = true
- 渲染器开启阴影
renderer.shadowMap.enabled = true
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.castShadow = true;
// ...
const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.5, 32, 32), material);
sphere.castShadow = true;
// ...
const plane = new THREE.Mesh(new THREE.PlaneGeometry(5, 5), material);
plane.receiveShadow = true;
// ...
默认情况下,阴影贴图的分辨率为512 * 512
,可以通过打印灯光的shadow
属性看到(阴影的相关属性存储在castShadow
的光源对象里)
可以通过修改mapSize
的分辨率来获取更清晰的阴影,比如将mapSize
设为2048 * 2048
:
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
// 也可以使用x,y设置
// directionalLight.shadow.mapSize.x = 2048;
// directionalLight.shadow.mapSize.y = 2048;
阴影很显然地变得更高清了
分辨率越高,意味着GPU的计算性能就会占用得越多,并且基于GPU得渲染原理,分辨率的值应该尽量传2的指数幂
观察阴影
观察阴影的媒介其实是摄像机,所以在平行光的位置会会存在一台摄像机
,通过directionalLight.shadow.camera
可以访问到,添加一个CameraHelper
可以看到这个摄像机的位置:
const directionalLightCameraHelper = new THREE.CameraHelper(
directionalLight.shadow.camera
);
scene.add(directionalLightCameraHelper);
所以,既然是正交摄像机,那么就可以设置它的near、far、left
等,目前默认情况下,摄像机的near = 0.5
,far = 500
,left = -5
,top = 5
,right = 5
,bottom = -5
,将这些值改为下面的配置:
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 6;
directionalLight.shadow.camera.top = 1;
directionalLight.shadow.camera.right = 1;
directionalLight.shadow.camera.bottom = -1;
directionalLight.shadow.camera.left = -1;
效果:
如果把摄像机的far设得更小,那就会发现阴影可能渲染得不完全:
directionalLight.shadow.camera.far = 3.8;
所以,如果未来遇到类似的这种阴影被切割或看不到阴影的问题,优先考虑是不是观察阴影的摄像机参数没有设对
不同的光源,使用的摄像机有可能也是不同的,可能是正交摄像机,也可能是透视摄像机
模糊
设置模糊属性要通过directionalLight.shadow.radius
来设置,值越大代表阴影越模糊,如:
directionalLight.shadow.radius = 100
阴影贴图算法
通过renderer.shadowMap.type
设置,可选的值有4个,性能表现依次下降:
比如降低原本阴影的分辨率:
directionalLight.shadow.mapSize.width = 256;
directionalLight.shadow.mapSize.height = 256;
// directionalLight.shadow.radius = 100;
使用BasicShadowMap
:
使用默认值PCFShadowMap
:
放大:
使用PCFSoftShadowMap
:
放大:
使用VSMShadowMap
:
阴影的其他实现方式
场景中如果实时计算的阴影太多,就会造成性能问题,所以还可以使用静态阴影贴图来实现
比如下面这张图是在3D软件中对球几何体模拟光照导出的阴影贴图
当位置正确时,阴影的效果是很好的
既然是静态阴影,那么当物体发生移动时,阴影的效果当然就不尽人意了
这种阴影属于BakedShadow
,可以理解为定制化的阴影,即3D建模完成后,阴影就已经定型了,不应该去动态的修改它,也不好修改
但如果是一个简单的alphaMap
阴影,那他修改起来就很方便,比如对于下面这张图:
想要在几何体下方创建一个plane
用来放置阴影图,首先创建一个plane
const sphereShadow = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: "red",
})
);
scene.add(sphere, sphereShadow, plane);
默认情况下这个创建出来的plane
是面对我们的:
通过将其绕X轴逆时针旋转Π / 2
可以得到朝上的平面
sphereShadow.rotateX(-Math.PI * 0.5);
将其放到物体下方,也就是底部平面正上方一点点,如:
sphereShadow.position.y = plane.position.y + 0.01;
这么做是避免出现渲染的冲突,即不应该把两个平面放在同一层,假如放在同一层,会出现下面的glitch
效果:
最后使用阴影图作为alpha
贴图即可,对于alphaMap
而言,贴图里白色的部分会渲染,黑色的部分会被忽略,注意要设置transparent :true
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/textures/simpleShadow.jpg");
const sphereShadow = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: "red",
transparent: true,
alphaMap: texture,
})
);
最后把material
的红色改为黑色即可:
那么,假如遇到物体移动的话,我们也可以很轻松的结合这个方式设置动态的阴影了
举个例子,假如物体在空间中沿着Y轴绕圈,阴影要跟着它动,可以这么实现:
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
controls.update();
// 球体绕圈旋转:
sphere.position.x = Math.cos(elapsedTime) * 1.5;
sphere.position.z = Math.sin(elapsedTime) * 1.5;
// 更新阴影位置:
sphereShadow.position.x = sphere.position.x;
sphereShadow.position.z = sphere.position.z;
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
再比如,如果要让球体弹跳,同时阴影有着明暗程度的变化,用现在的阴影贴图也可以很轻易地实现:
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
// 球体绕圈旋转:
sphere.position.x = Math.cos(elapsedTime) * 1.5;
sphere.position.z = Math.sin(elapsedTime) * 1.5;
// 球体弹跳:
sphere.position.y = Math.abs(Math.sin(elapsedTime * 3));
// 更新阴影位置:
sphereShadow.position.x = sphere.position.x;
sphereShadow.position.z = sphere.position.z;
// 更新阴影的透明度:
sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3;
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
阴影的实现方案要根据实际场景来决定,贴图的简单动画相比动态的实时计算性能更好,有时牺牲一些物理上的真实性对用户的感知来说其实并不会有太大的影响
搭鬼屋demo记录的一些API
git@github.com:JohnWicc/threejs-hunted-house.git
生成雾
有线性雾和指数雾FogExp2
,区别在于雾的变化是线性变化程度还是指数变化程度,都是越远雾越蒙
const fog = new THREE.Fog("#262837", 1, 15);
scene.fog = fog;
setClearColor
可以理解为设置天空颜色,与scene.background
在设置颜色时效果相同
// scene.background = new THREE.Color('#red') // 等价
renderer.setClearColor("red", 0.5);
clock类
threejs
内置的时钟,通常用来实现动画的计算,最常用的API是getElapsedTime
,可以获取此时此刻的秒数
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// update Ghosts
const ghost1Angle = elapsedTime * 0.5;
ghost1.position.x = Math.cos(ghost1Angle) * 4;
ghost1.position.z = Math.sin(ghost1Angle) * 4;
ghost1.position.y = Math.sin(ghost1Angle * 3);
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();