初始代码
import "./style.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import * as dat from "dat.gui";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
/**
* Base
*/
// Debug
const gui = new dat.GUI();
// Canvas
const canvas = document.querySelector("canvas.webgl");
// Scene
const scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight("#ffffff", 3);
dirLight.position.set(0.25, 3, -2.25);
scene.add(dirLight);
/**
* Test sphere
*/
const testSphere = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
new THREE.MeshStandardMaterial()
)
scene.add(testSphere)
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener("resize", () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
);
camera.position.set(4, 1, -4);
scene.add(camera);
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* Animate
*/
const tick = () => {
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
gui
.add(dirLight, "intensity")
.min(0)
.max(10)
.step(0.001)
.name("lightIntensity");
gui.add(dirLight.position, "x").min(-5).max(5).step(0.001).name("lightX");
gui.add(dirLight.position, "y").min(-5).max(5).step(0.001).name("lightY");
gui.add(dirLight.position, "z").min(-5).max(5).step(0.001).name("lightZ");
: Boolean 默认false ,物理的正确的照明方式
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.physicallyCorrectLights=true
const gltfLoader=new GLTFLoader()
gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
const helmet = gltf.scene;
helmet.scale.set(10, 10, 10);
helmet.position.set(0, -4, 0);
helmet.rotation.y = THREE.MathUtils.degToRad(90);
scene.add(helmet);
gui
.add(helmet.rotation, "y")
.min(-Math.PI)
.max(Math.PI)
.step(0.001)
.name("rotation");
});
渲染环境
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMap = 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",
]);
scene.background=envMap
callback - 以一个object3D对象作为第一个参数的函数。
通过此函数我们可以遍历加载的对象 从而分析出加载模型的类型
目的 在以后的每个分部分上可以添加阴影等其他效果
1.创建函数
const updateAllMats = () => {
scene.traverse((child) => {
console.log(child)
});
};
2.调用函数
gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
const helmet = gltf.scene;
helmet.scale.set(10, 10, 10);
helmet.position.set(0, -4, 0);
helmet.rotation.y = THREE.MathUtils.degToRad(90);
scene.add(helmet);
updateAllMats()
gui
.add(helmet.rotation, "y")
.min(-Math.PI)
.max(Math.PI)
.step(0.001)
.name("rotation");
});
通过判断类型可以给 mesh加入envmap材质
const updateAllMats = () => {
scene.traverse((child) => {
console.log(child)
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
child.material.envMap = envMap;
child.material.needsUpdate = true;
child.castShadow = true;
child.receiveShadow = true;
}
});
};
加入后效果变化不是很明显
调节envMapIntensity可以看到效果
outputEncoding
属性控制输出渲染编码。默认情况下,outputEncoding的值为THREE.LinearEncoding
,看起来还行但是不真实,建议将值改为THREE.sRGBEncoding
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.physicallyCorrectLights=true
renderer.outputEncoding = THREE.sRGBEncoding
除此之外还有另一个属性值为THREE.GammaEncoding,这种编码的优点在于它允许我们使用一种表现像亮度brightness的叫gammaFactor的值。GammaEncoding是一种存储颜色的方法,根据人眼的敏感度优化明暗值的存储方式。当使用sRGBEncoding时,其实就像使用默认gammaFactor值为2.2的GammaEncoding。
下面链接可以提供更多信息关于GammaEncoding和sRGBEncodingColor management in three.js
https://medium.com/game-dev-daily/the-srgb-learning-curve-773b7f68cf7a
尽管这样就可能会有人认为GammaEncoding优于sRGBEncoding,因为我们可以在更暗或更亮的场景里控制gammaFactor,但是实际上这样做在物理层面上并不正确,下面会讲到如何更好管理亮度brightness
我们可以发现设置完渲染器的输出编码outputEncoding为THREE.sRGBEncoding后,我们的环境贴图颜色也改变了,虽然看起来效果不错,但我们还是要选择保留其原先正确的颜色。问题就在于我们设置完渲染器的输出编码之后,环境贴图的纹理还是默认的THREE.LinearEncoding。
其实规则很直接,所有我们能够直接看到的纹理贴图,比如map,就应该使用THREE.sRGBEncoding作为编码;而其他的纹理贴图比如法向纹理贴图normalMap就该使用THREE.LinearEncoding。
我们可以直接看到环境贴图,所以应该将其编码设为THREE.sRGBEncoding
renderer.toneMapping = THREE.ACESFilmicToneMapping
色调映射Tone mapping旨在将超高的动态范围HDR转换到我们日常显示的屏幕上的低动态范围LDR的过程。
说明一下HDR和LDR(摘自知乎LDR和HDR):因为不同的厂家生产的屏幕亮度(物理)实际上是不统一的,那么我们在说LDR时,它是一个0到1范围的值,对应到不同的屏幕上就是匹配当前屏幕的最低亮度(0)和最高亮度(1)
自然界中的亮度差异是非常大的。例如,蜡烛的光强度大约为15,而太阳光的强度大约为10w。这中间的差异是非常大的,有着超级高的动态范围。
我们日常使用的屏幕,其最高亮度是经过一系列经验积累的,所以使用、用起来不会对眼睛有伤害;但自然界中的,比如我们直视太阳时,实际上是会对眼睛产生伤害的。
那为了改变色调映射tone mapping,则要更新WebGLRenderer上的toneMapping属性,有以下这些值THREE.NoToneMapping (默认)
THREE.LinearToneMapping
THREE.ReinhardToneMapping
THREE.CineonToneMapping
THREE.ACESFilmicToneMapping
尽管我们的贴图不是HDR,但使用tone mapping可以塑造更真实的效果。
我们也可以通过GUI 来改变查看效果对比
gui.add(renderer,"toneMapping",{
No: THREE.NoToneMapping ,
Linear:THREE.LinearToneMapping,
Reinhard:THREE.ReinhardToneMapping,
CineonTone:THREE.CineonToneMapping,
ACESFilmicTone:THREE.ACESFilmicToneMapping
}).onFinishChange(()=>{
renderer.toneMapping=Number(renderer.toneMapping)
})
但是这个效果只在环境上出现如果我们需要模型也相应的变化需要 在完成时调用updateAllMats
同时让材质开启更新
child.material.needsUpdate = true;
const updateAllMats = () => {
scene.traverse((child) => {
console.log(child)
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
child.material.envMap = envMap;
child.material.needsUpdate = true;
child.material.envMapIntensity=debugObject.envMapIntensity
child.castShadow = true;
child.receiveShadow = true;
}
});
};
....
gui.add(renderer,"toneMapping",{
No: THREE.NoToneMapping ,
Linear:THREE.LinearToneMapping,
Reinhard:THREE.ReinhardToneMapping,
CineonTone:THREE.CineonToneMapping,
ACESFilmicTone:THREE.ACESFilmicToneMapping
}).onFinishChange(()=>{
renderer.toneMapping=Number(renderer.toneMapping)
updateAllMats()
})
gui.add(renderer,"toneMappingExposure").min(0).max(10).step(0.01).name("toneMappingExposure")
当我们渲染的时候不开启 antialias
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
当开启之后
明显消除了锯齿化效果
dirLight.castShadow=truedirLight.shadow.mapSize.set(1024,1024)
dirLight.shadow.camera.far=15
dirLight.shadow.normalBias=0.05
...
...
const updateAllMats = () => {
scene.traverse((child) => {
console.log(child)
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
child.material.envMap = envMap;
child.material.needsUpdate = true;
child.material.envMapIntensity=debugObject.envMapIntensity
child.castShadow = true;
child.receiveShadow = true;
}
});
};
...
...
renderer.castShadow=true
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;