材料
材质用于在几何图形的每个可见像素上添加颜色。
Three.js 有许多带有预制着色器的内置材质。
创建3个网格 由3种不同的几何形状(球体、平面和圆环)组成并使用相同的MeshBasicMaterial在所有 3 上
/**
* Objects
*/
const material = new THREE.MeshBasicMaterial()
const sphere = new THREE.Mesh(
new THREE.SphereBufferGeometry(0.5, 16, 16),
material
)
sphere.position.x = - 1.5
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
material
)
const torus = new THREE.Mesh(
new THREE.TorusBufferGeometry(0.3, 0.2, 16, 32),
material
)
torus.position.x = 1.5
scene.add(sphere, plane, torus) // add(...) 方法支持一次添加多个对象
在tick函数上旋转对象
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update objects
sphere.rotation.y = 0.1 * elapsedTime
plane.rotation.y = 0.1 * elapsedTime
torus.rotation.y = 0.1 * elapsedTime
sphere.rotation.x = 0.15 * elapsedTime
plane.rotation.x = 0.15 * elapsedTime
torus.rotation.x = 0.15 * elapsedTime
// ...
}
所有纹理图像都位于该 /static/textures/ 文件夹中
确保在实例化之前这样做 material:
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const doorColorTexture = textureLoader.load('/textures/door/color.jpg')
const doorAlphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const doorAmbientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const doorHeightTexture = textureLoader.load('/textures/door/height.jpg')
const doorNormalTexture = textureLoader.load('/textures/door/normal.jpg')
const doorMetalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const doorRoughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
const matcapTexture = textureLoader.load('/textures/matcaps/1.png')
const gradientTexture = textureLoader.load('/textures/gradients/3.jpg')
为确保所有纹理都加载良好,您可以将它们用于具有 map 属性的材质
const material = new THREE.MeshBasicMaterial({ map: doorColorTexture })
MeshBasicMaterial
MeshBasicMaterial 可能是最“基本”的材料
// 在对象的材质实例化时设置材质属性
const material = new THREE.MeshBasicMaterial({
map: doorColorTexture
})
// Equals
const material = new THREE.MeshBasicMaterial()
material.map = doorColorTexture
- 该map属性将在几何体表面上应用纹理:
material.map = doorColorTexture
- 该 color 属性将在几何图形的表面上应用统一的颜色。当您 color 直接更改属性时,您必须实例化一个Color类。您可以使用许多不同的格式:
material.color = new THREE.Color('#ff0000')
material.color = new THREE.Color('#f00')
material.color = new THREE.Color('red')
material.color = new THREE.Color('rgb(255, 0, 0)')
material.color = new THREE.Color(0xff0000)
- 无论相机的距离如何,该 wireframe 属性都将以 1px 的细线显示构成几何图形的三角形:
material.wireframe = true
- 该 opacity 属性控制透明度,但要工作,您应该将 transparent 属性设置 true 为通知 Three.js 此材质现在支持透明度:
material.transparent = true
material.opacity = 0.5
现在透明度正在起作用,我们可以使用该 alphaMap 属性通过纹理来控制透明度:
material.transparent = true
material.alphaMap = doorAlphaTexture
- 该 side 属性可让您决定面的哪一侧可见。默认情况下,正面是可见的 ( THREE.FrontSide),但您可以改为显示背面 ( THREE.BackSide) 或两者都显示 ( THREE.DoubleSide):
material.side = THREE.DoubleSide
MeshNormalMaterial
MeshNormalMaterial显示出漂亮的紫色、蓝色、绿色,看起来就像我们在纹理课程中看到的正常纹理
const material = new THREE.MeshNormalMaterial()
法线是编码在每个顶点中的信息,包含面部外侧的方向。如果将这些法线显示为箭头,则会从构成几何体的每个顶点中得到直线。
您可以将法线用于许多事情,例如计算如何照亮面部或环境应如何在几何体表面上反射或折射。
使用MeshNormalMaterial时,颜色将仅显示法线相对于相机的方向。如果您围绕球体旋转,您会看到颜色始终相同,无论您正在查看的是球体的哪个部分。
虽然您可以使用我们在MeshBasicMaterial中发现的一些属性像 wireframe、 和 一样transparent, 还有一个可以使用的新属性,称为 :opacitysideflatShading
material.flatShading = true
flatShading 将使面变平,这意味着法线不会在顶点之间插值。
MeshNormalMaterial可用于调试法线,但它看起来也很棒,您可以像 ilithya 在她的投资组合中所做的那样使用它https://www.ilithya.rocks
MeshMatcap材质
MeshMatcapMaterial是一种非常棒的材料,因为它看起来非常棒,而且性能非常好。
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture
网格将看起来被照亮,但它只是一个看起来像它的纹理。
唯一的问题是,无论相机方向如何,幻觉都是相同的。此外,您无法更新灯光,因为没有灯光。
还可以使用 3D 软件通过在相机前面以方形图像渲染球体来创建自己的 matcaps。
最后,可以尝试在 Photoshop 等 2D 软件中制作 matcap
庞大的matcap列表:https 😕/github.com/nidorx/matcaps
MeshDepthMaterial
MeshDepthMaterial 如果它接近相机的值,它将简单地将几何图形着色为白色,如果它接近相机的 near 值,则将其着色为黑色 far:
const material = new THREE.MeshDepthMaterial()
添加一些灯
以下材料需要灯光才能看到。让我们在场景中添加两个简单的灯光。
创建一个AmbientLight并将其添加到场景中:
/**
* Lights
*/
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
创建一个PointLight并将其添加到场景中:
// ...
const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)
网状Lambert材料
MeshLambert材料是我们将要使用的第一种对光有反应的材料:
const material = new THREE.MeshLambertMaterial()
网状Lambert材料支持与MeshBasicMaterial相同的属性还有一些与灯光相关的属性。
MeshLambert材料是使用灯光的性能最高的材料。不幸的是,这些参数并不方便,如果你仔细观察像球体这样的圆形几何图形,你会在几何图形上看到奇怪的图案。
MeshPhong材料
MeshPhong材料与MeshLambertMaterial非常相似 ,但奇怪的图案不太明显,还可以看到几何体表面的光反射:
const material = new THREE.MeshPhongMaterial()
MeshPhong材料性能不如MeshLambertMaterial,但是,在这个级别上并不重要。
shininess 您可以使用该属性控制光反射 。值越高,表面越亮。您还可以使用以下 specular 属性更改反射的颜色:
material.shininess = 100
material.specular = new THREE.Color(0x1188ff)
光反射将具有蓝色
MeshToonMaterial
MeshToonMaterial类似于MeshLambertMaterial 在属性方面,但具有卡通风格:
const material = new THREE.MeshToonMaterial()
默认情况下,您只会获得两部分着色(一份用于阴影,一份用于灯光)。要为着色添加更多步骤,您可以使用该 gradientMap 属性并使用 gradientTexture 在课程开始时加载的:
material.gradientMap = gradientTexture // 使用的渐变纹理很小,卡通效果不再起作用
如果您对此进行测试,您会发现卡通效果不再起作用。那是因为我们使用的渐变纹理很小,并且该纹理的像素是混合的。是的,这是一个问题 , minFilter就像 我们在纹理课程中看到的那样。magFiltermipmapping
要解决这个问题,我们可以简单地将 minFilter and 更改magFilter 为 THREE.NearestFilter。
Using THREE.NearestFilter 表示我们没有使用 mip 映射,我们可以使用以下命令停用它 gradientTexture.generateMipmaps = false:
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false // 表示我们没有使用 mip 映射
您现在应该看到带有中间步骤的卡通效果。
您可以使用位于以下位置的图像尝试更多步骤 /static/textures/gradients.5.jpg:
const gradientTexture = textureLoader.load('/textures/gradients/5.jpg')
网格标准材料
MeshStandardMaterial使用基于物理的渲染原理。是的,我们谈论的是我们在纹理课程中看到的 PBR。像MeshLambertMaterial 和MeshPhongMaterial ,它支持灯光,但具有更逼真的算法和更好的参数,如粗糙度和金属度。
之所以称为“标准”,是因为 PBR 正在成为许多软件、引擎和库中的标准。我们的想法是使用真实的参数获得真实的结果,无论您使用何种技术,您都应该获得非常相似的结果:
const material = new THREE.MeshStandardMaterial()
material.metalness = 0.45
material.roughness = 0.65
添加调试界面
npm install --save dat.gui
import * as dat from 'dat.gui'
/**
* Debug
*/
const gui = new dat.GUI()
// ...
// 添加调整(在创建材料之后)
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)
material.map = doorColorTexture
// 复制了 uv 属性
sphere.geometry.setAttribute('uv2', new THREE.BufferAttribute(sphere.geometry.attributes.uv.array, 2))
plane.geometry.setAttribute('uv2', new THREE.BufferAttribute(plane.geometry.attributes.uv.array, 2))
torus.geometry.setAttribute('uv2', new THREE.BufferAttribute(torus.geometry.attributes.uv.array, 2))
可以添加 aoMap 使用 doorAmbientOcclusionTexture 纹理并使用 aoMapIntensity 属性控制强度:
material.aoMap = doorAmbientOcclusionTexture
material.aoMapIntensity = 1
裂缝应该看起来更暗,这会产生对比并增加尺寸。
该 displacementMap 属性将移动顶点以创建真正的浮雕:
material.displacementMap = doorHeightTexture
它应该看起来很糟糕。这是因为我们的几何体上缺少顶点(我们需要更多的细分)并且位移太强了:
material.displacementScale = 0.05
// ...
new THREE.SphereBufferGeometry(0.5, 64, 64),
// ...
new THREE.PlaneBufferGeometry(1, 1, 100, 100),
// ...
new THREE.TorusBufferGeometry(0.3, 0.2, 64, 128),
我们可以使用and ,而不是为整个几何体 指定 uniform metalness 和 :roughnessmetalnessMaproughnessMap
material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture
反射看起来很奇怪,因为 metalness 和 roughness 属性仍然分别影响每个地图。我们应该评论它们或使用它们的原始值:
material.metalness = 0
material.roughness = 1
normalMap 无论细分如何,都会伪造法线方向并在曲面上添加细节:
material.normalMap = doorNormalTexture
normalScale 您可以使用该属性更改法线强度 。小心,它是一个Vector2:
material.normalScale.set(0.5, 0.5)
MeshPhysicalMaterial
MeshPhysicalMaterial与MeshStandardMaterial相同,但支持透明涂层效果。您可以控制透明涂层的属性
PointsMaterial
您可以将PointsMaterial与粒子一起使用
ShaderMaterial 和 RawShaderMaterial
ShaderMaterial和RawShaderMaterial都可以用来创建您自己的材质
环境图
环境贴图就像场景周围的图像。您可以使用它为对象添加反射或折射。它也可以用作照明信息。
const material = new THREE.MeshStandardMaterial()
material.metalness = 0.7
material.roughness = 0.2
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)
要将环境贴图添加到我们的材质中,我们必须使用该 envMap 属性。Three.js 只支持立方体环境贴图。Cube 环境贴图是 6 个图像,每个图像对应于环境的一侧。
/static/textures/environmentMap/ 您可以在文件夹中找到多个环境贴图 。
要加载立方体纹理,您必须使用CubeTextureLoader 而不是TextureLoader
实例化CubeTextureLoader 一在实例化 material and 调用它的 load(…) 方法之前,但是使用一个路径数组而不是一个路径:
const cubeTextureLoader = new THREE.CubeTextureLoader()
const environmentMapTexture = cubeTextureLoader.load([
'/textures/environmentMaps/0/px.jpg',
'/textures/environmentMaps/0/nx.jpg',
'/textures/environmentMaps/0/py.jpg',
'/textures/environmentMaps/0/ny.jpg',
'/textures/environmentMaps/0/pz.jpg',
'/textures/environmentMaps/0/nz.jpg'
])
您现在可以使用材料的 environmentMapTexture in envMap 属性:
material.envMap = environmentMapTexture
3D文本
Three.js 已经通过TextBufferGeometry支持 3D 文本几何 2班级。问题是你必须指定一种字体,而且这种字体必须是一种特殊的 json 格式,称为字体
如何获得字体字体
import typefaceFont from 'three/examples/fonts/helvetiker_regular.typeface.json'
加载字体
要加载字体,我们必须使用一个名为FontLoader的新加载器类, 这个加载器就像TextureLoader一样工作. 在该部分后添加以下代码 textureLoader (如果您使用的是其他字体,请不要忘记更改路径):
/**
* Fonts
*/
const fontLoader = new THREE.FontLoader()
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
{
console.log('loaded')
}
)
创建几何
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
{
const textGeometry = new THREE.TextBufferGeometry(
'Hello Three.js',
{
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
}
)
const textMaterial = new THREE.MeshBasicMaterial()
const text = new THREE.Mesh(textGeometry, textMaterial)
scene.add(text)
}
)
如果您想看到一些很酷的东西,请添加 wireframe: true 到您的材料中。
const textMaterial = new THREE.MeshBasicMaterial({ wireframe: true })
将文本居中
可以让 Three.js 通过调用 computeBoundingBox() 几何体来计算这个框的边界:
textGeometry.computeBoundingBox()
获取可以用几何上的属性:
console.log(textGeometry.boundingBox)
可以在 translate(…) 方法之后立即在几何上 使用该computeBoundingBox() 方法:
textGeometry.translate(
- textGeometry.boundingBox.max.x * 0.5,
- textGeometry.boundingBox.max.y * 0.5,
- textGeometry.boundingBox.max.z * 0.5
)
文本应该居中,但如果你想非常精确,你还应该减去 bevelSize which is 0.02:
textGeometry.translate(
- (textGeometry.boundingBox.max.x - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.y - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.z - 0.03) * 0.5 // Subtract bevel thickness
)
center() 我们在这里所做的实际上可以通过调用几何上的方法来更快地完成 :
textGeometry.center()
添加matcap材质
将使用MeshMatcapMaterial,因为它看起来很酷,而且性能很好。
可以从此存储库下载一个https://github.com/nidorx/matcaps,不要花太多时间选择它!如果不是供个人使用,请确保您有权使用它。您不需要高分辨率纹理, 256x256 应该绰绰有余。
现在可以使用TextureLoader加载纹理 一已经在代码中:
const matcapTexture = textureLoader.load('/textures/matcaps/1.png')
现在可以替换丑陋的MeshBasicMaterial 由漂亮的MeshMatcapMaterial 一并将我们的matcapTexture变量与 matcap属性一起使用:
const textMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
添加对象
for(let i = 0; i < 100; i++)
{
const donutGeometry = new THREE.TorusBufferGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
// 随机位置
donut.position.x = (Math.random() - 0.5) * 10
donut.position.y = (Math.random() - 0.5) * 10
donut.position.z = (Math.random() - 0.5) * 10
// 随机旋转
donut.rotation.x = Math.random() * Math.PI
donut.rotation.y = Math.random() * Math.PI
// 随机尺寸
const scale = Math.random()
donut.scale.set(scale, scale, scale)
scene.add(donut)
}
优化
将 donutGeometry and donutMaterial 移出循环:
const donutGeometry = new THREE.TorusBufferGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
for(let i = 0; i < 100; i++)
{
// ...
}
删除 donutMaterial,重命名 textMaterial by material 并将其用于 the text 和 donut:
const material = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
// ...
const text = new THREE.Mesh(textGeometry, material)
// ...
for(let i = 0; i < 100; i++)
{
const donut = new THREE.Mesh(donutGeometry, material)
// ...
}
灯
环境光
AmbientLight在场景的所有几何形状上应用全向照明。第一个参数是 color ,第二个参数是 intensity。至于材质,可以在实例化的时候直接设置属性,也可以在之后修改:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
// Equals
const ambientLight = new THREE.AmbientLight()
ambientLight.color = new THREE.Color(0xffffff)
ambientLight.intensity = 0.5
scene.add(ambientLight)
就像我们为材料所做的那样,您可以将属性添加到调试 UI。我们不会在本课的其余部分中这样做,但如果您想简化测试,请随时添加调整:
gui.add(ambientLight, ‘intensity’).min(0).max(1).step(0.001)
定向光
定向光 将产生类似太阳的效果,就好像太阳光线平行传播一样。第一个参数是 color ,第二个参数是 intensity:
const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.3)
scene.add(directionalLight)
默认情况下,灯光似乎来自上方。要改变这一点,您必须使用该属性来移动整个灯光, position 就像它是一个普通对象一样。
directionalLight.position.set(1, 0.25, 0)
半球光
半球光 类似于AmbientLight,但来自天空的颜色与来自地面的颜色不同。面向天空的面将被一种颜色照亮,而另一种颜色将照亮面向地面的面。
第一个参数是 color 对应的天空颜色,第二个参数是 groundColor ,第三个参数是 intensity:
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3)
scene.add(hemisphereLight)
点光源
点光源 几乎就像一个打火机。光源无限小,光线均匀地向各个方向传播。第一个参数是 color ,第二个参数是 intensity:
const pointLight = new THREE.PointLight(0xff9000, 0.5)
scene.add(pointLight)
可以像任何对象一样移动它:
pointLight.position.set(1, - 0.5, 1)
默认情况下,光强度不会减弱。distance 但是您可以使用和 decay属性控制该淡入淡出距离以及淡入淡出的速度 。您可以在类的参数中将它们设置为第三和第四个参数,或者在实例的属性中:
const pointLight = new THREE.PointLight(0xff9000, 0.5, 10, 2)
矩形区域光
矩形区域光就像您可以在拍摄组上看到的大矩形灯一样工作。它是定向光和漫射光的混合。第一个参数是 color,第二个参数是 intensity,第四个参数是 width 矩形,第四个参数是它的 height:
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
scene.add(rectAreaLight)
矩形区域光仅适用于MeshStandardMaterial和MeshPhysicalMaterial
可以移动灯光并旋转它(Vector3):
rectAreaLight.position.set(- 1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())
聚光灯
聚光灯 像手电筒一样工作。它是一个从一个点开始并朝向一个方向的光锥。以下是其参数列表:
- color: 颜色
- intensity: 力量
- distance:强度下降到的距离 0
- angle: 光束有多大
- penumbra: 光束轮廓的扩散程度
- decay: 灯光变暗的速度有多快
const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)
旋转SpotLight来改变位置,记得要将其target添加到场景中
spotLight.target.position.x = - 0.75
scene.add(spotLight.target) // 注意将其target添加到场景中
表现
灯光很棒,如果使用得当,可以很逼真。问题是灯光在性能方面可能会花费很多。GPU 必须进行许多计算,例如从面部到灯光的距离、面部朝向灯光的距离、面部是否在聚光灯锥中等。
尝试添加尽可能少的灯,并尝试使用成本更低的灯。
最低成本:
- 环境光
- 半球光
成本适中:
- 定向光
- 点光源
成本高:
- 聚光灯
- 矩形区域光
烘烤
一种很好的照明技术称为烘焙。这个想法是你将光烘焙到纹理中。这可以在 3D 软件中完成。不幸的是,您将无法移动灯光,因为没有灯光,而且您可能需要很多纹理。
帮手
定位和定向灯光很困难。为了帮助我们,我们可以使用助手。仅支持以下帮助程序:
半球LightHelper 2
定向光助手
点光源助手
RectAreaLightHelper
聚光灯助手
要使用它们,只需实例化这些类。使用相应的灯光作为参数,并将它们添加到场景中。第二个参数使您能够更改助手的 size:
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)
对于SpotLightHelper,没有 size 参数。此外,移动目标后,您需要 update(…) 在下一帧调用该方法:
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)
window.requestAnimationFrame(() =>
{
spotLightHelper.update()
})
RectAreaLightHelper更难使用。现在,该类不是 THREE 变量的一部分。必须像对OrbitControls examples所做的那样从依赖项中导入它:
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper
// 使用它
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)
遗憾的是,就像SpotLightHelper一样,必须在下一帧更新它 position 并 rotation 手动更新它:
window.requestAnimationFrame(() =>
{
rectAreaLightHelper.position.copy(rectAreaLight.position)
rectAreaLightHelper.quaternion.copy(rectAreaLight.quaternion)
rectAreaLightHelper.update()
})
阴影
激活阴影贴图
激活阴影贴图 renderer:
renderer.shadowMap.enabled = true
然后,需要遍历场景中的每个对象,判断对象是否可以通过该 castShadow 属性投射阴影,以及该对象是否可以通过该属性接收阴影 receiveShadow 。
尝试在尽可能少的对象上激活这些:
sphere.castShadow = true
// ...
plane.receiveShadow = true
castShadow 最后,使用该属性激活灯光上的阴影 。
只有以下类型的灯光支持阴影:
directionalLight.castShadow = true
阴影贴图优化
渲染大小
Three.js 正在为每个灯光进行称为阴影贴图的渲染。shadow 您可以使用灯光上的属性访问此阴影贴图(和许多其他内容) :
console.log(directionalLight.shadow)
至于渲染,我们需要指定一个大小。默认情况下,阴影贴图大小仅 512x512 出于性能原因。我们可以改进它,但请记住,mipmapping 需要 2 的幂:
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024
阴影应该已经看起来更好了
近与远
Three.js 使用相机来渲染阴影贴图。这些相机与已经使用的相机具有相同的属性。这意味着我们必须定义 a near 和 a far。它不会真正提高阴影的质量,但它可能会修复看不到阴影或阴影突然被裁剪的错误。
为了帮助我们调试相机并预览 near and far,我们可以使用CameraHelper以及用于位于 directionalLight.shadow.camera 属性中的阴影贴图的相机:
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)
现在可以直观地看到相机的 near 和 far 。尝试找到适合场景的值:
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6
振幅
使用我们刚刚添加的相机助手,我们可以看到相机的幅度太大。
因为我们使用的是DirectionalLight,Three.js 使用的是OrthographicCamera。top如果您还记得相机课程,我们可以使用、 right、 bottom和 left 属性控制相机在每一侧可以看到的距离 。让我们减少这些属性:
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = - 2
directionalLight.shadow.camera.left = - 2
值越小,阴影就越精确。但如果它太小,阴影将被裁剪。
完成后,可以隐藏相机助手:
directionalLightCameraHelper.visible = false
模糊
您可以使用以下属性控制阴影模糊 radius :
directionalLight.shadow.radius = 10
此技术不使用相机与对象的接近度。这只是一般且廉价的模糊。
阴影贴图算法
不同类型的算法可以应用于阴影贴图:
- THREE.BasicShadowMap 性能非常好但质量很差
- THREE.PCFShadowMap 性能较差但边缘更平滑
- THREE.PCFSoftShadowMap 性能较差但边缘更柔和
- THREE.VSMShadowMap 性能较低,约束较多,可能会产生意想不到的结果
要更改它,请更新 renderer.shadowMap.type 属性。默认是 THREE.PCFShadowMap ,但您可以使用 THREE.PCFSoftShadowMap以获得更好的质量。
renderer.shadowMap.type = THREE.PCFSoftShadowMap
聚光灯
尝试添加一个SpotLight 就像我们在 灯光 课中所做的那样,将 castShadow 属性添加到 true. 不要忘记将 target属性添加到 scene.
还将添加一个相机助手:
// Spot light
const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3)
spotLight.castShadow = true
spotLight.position.set(0, 2, 2)
scene.add(spotLight)
scene.add(spotLight.target)
const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)
如果场景太亮,可以降低其他灯光强度:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
// ...
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)
如所见,阴影不能很好地融合。它们是独立处理的,不幸的是,没有什么可做的。
但是我们可以使用与定向光相同的技术来提高阴影质量。
更改 shadow.mapSize:
spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024
因为我们现在使用的是SpotLight ,在内部,Three.js 正在使用PerspectiveCamera。这意味着我们必须更改属性, 而不是top、 right、 bottom和 属性。尝试在不裁剪阴影的情况下找到尽可能小的角度:leftfov
spotLight.shadow.camera.fov = 30
更改 near 和 far 值:
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6
完成后,可以隐藏相机助手:
spotLightCameraHelper.visible = false
点光源
让我们试试最后一个支持阴影的光,PointLight :
// Point light
const pointLight = new THREE.PointLight(0xffffff, 0.3)
pointLight.castShadow = true
pointLight.position.set(- 1, 1, 0)
scene.add(pointLight)
const pointLightCameraHelper = new THREE.CameraHelper(pointLight.shadow.camera)
scene.add(pointLightCameraHelper)
如果场景太亮,您可以降低其他灯光强度:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3)
// ...
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3)
// ...
const spotLight = new THREE.SpotLight(0xffffff, 0.3, 10, Math.PI * 0.3)
如您所见,相机助手是一个PerspectiveCamera(就像SpotLight ) 但面朝下。这是由于 Three.js 如何处理PointLight的阴影贴图.
因为点光源向各个方向照明,Three.js 必须渲染 6 个方向中的每一个来创建立方体阴影贴图。您看到的相机助手是相机在这 6 个渲染中的最后一个(向下)的位置。
进行所有这些渲染可能会产生性能问题。尽量避免点光源过多启用阴影。
您可以在此处调整的唯一属性是 mapSize, near 和 far:
pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024
pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5
完成后,可以隐藏相机助手:
pointLightCameraHelper.visible = false
烘焙阴影
以简单地在渲染器中停用它们,而不是注释所有与阴影相关的代码行:
renderer.shadowMap.enabled = false
在创建对象和灯光之前添加以下代码:
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(5, 5),
new THREE.MeshBasicMaterial({
map: bakedShadow
})
)
烘焙阴影替代品
一个不太现实但更动态的解决方案是在球体下方和平面上方使用更简单的阴影。
纹理是一个简单的光环。白色部分可见,黑色部分不可见。
然后,我们用球体移动那个阴影。
首先,让我们通过放回MeshStandardMaterial来移除之前的烘焙阴影 一在飞机上:
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(5, 5),
material
)
然后,可以加载位于 /static/textures/backedShadow.jpg.
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
可以使用一个简单的平面来创建阴影,旋转并放置在地板上方。材质必须为黑色,但阴影纹理为 alphaMap. 不要忘记更改 transparent 为 true,并将网格添加到 scene:
const sphereShadow = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
alphaMap: simpleShadow
})
)
sphereShadow.rotation.x = - Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphere, sphereShadow, plane)
如果要为球体设置动画,可以简单地为阴影设置相应的动画并根据球体的高度更改其不透明度:
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update the sphere
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))
// Update the shadow
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3
// ...
}
tick()