场景
主要就是父子关系坐标系关系。直接上例子
import * as THREE from "../../three/build/three";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setClearColor(0xaaaaaa);
renderer.shadowMap.enable = true; //开启阴影贴图
function makeCamera(fov = 40) {
const aspect = 2;
const Znear = 0.1;
const ZFar = 1000;
return new THREE.PerspectiveCamera(fov, aspect, Znear, ZFar);
}
const camera = makeCamera();
camera.position.set(8, 4, 10).multiplyScalar(3);
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
{
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 20, 0);
scene.add(light);
light.castShadow = true;
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
const d = 50;
light.shadow.camera.left = -d;
light.shadow.camera.right = d;
light.shadow.camera.top = d;
light.shadow.camera.bottom = -d;
light.shadow.camera.near = 1;
light.shadow.camera.far = 50;
light.shadow.bias = 0.001;
}
{
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 2, 4);
scene.add(light);
}
const groundGeometry = new THREE.PlaneGeometry(50, 50);
const groundMaterial = new THREE.MeshPhongMaterial({ color: 0xcc8866 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = Math.PI * -0.5;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
const [carWidth, carHeight, carLength] = [4, 1, 8];
const tank = new THREE.Object3D();
scene.add(tank);
const bodyGeometry = new THREE.BoxGeometry(carWidth, carHeight, carLength);
const bodyMaterial = new THREE.MeshPhongMaterial({ color: 0x6688aa });
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
bodyMesh.position.y = 1.4;
bodyMesh.castShadow = true;
tank.add(bodyMesh);
const tankCameraFov = 75;
const tankCamera = makeCamera(tankCameraFov);
tankCamera.position.set(0, 3, -6);
tankCamera.rotation.y = Math.PI;
bodyMesh.add(tankCamera);
const [wheelRadius, wheelThickness, wheelSegments] = [1, 0.5, 6];
const wheelGeometry = new THREE.CylinderGeometry(
wheelRadius,
wheelRadius,
wheelThickness,
wheelSegments
);
const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x888888 });
const wheelPositions = [
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, carLength / 3],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, carLength / 3],
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, 0],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, 0],
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, -carLength / 3],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, -carLength / 3],
];
const wheelMeshes = wheelPositions.map((position) => {
const mesh = new THREE.Mesh(wheelGeometry, wheelMaterial);
mesh.position.set(...position);
mesh.rotation.z = Math.PI * 0.5;
mesh.castShadow = true;
bodyMesh.add(mesh);
return mesh;
});
const [
domeRadius,
domeWidthSubdivisions,
domeHeightSubdivisions,
domePhiStart,
domePhiEnd,
domeThetaStart,
domeThetaEnd,
] = [2, 12, 12, 0, Math.PI * 2, 0, Math.PI * 0.5];
const domeGeometry = new THREE.SphereGeometry(
domeRadius,
domeWidthSubdivisions,
domeHeightSubdivisions,
domePhiStart,
domePhiEnd,
domeThetaStart,
domeThetaEnd
);
const domeMesh = new THREE.Mesh(domeGeometry, bodyMaterial);
domeMesh.castShadow = true;
bodyMesh.add(domeMesh);
domeMesh.position.y = 0.5;
const [turretWidth, turretHeight, turretLength] = [
0.1,
0.1,
carLength * (0.75 * 0.2),
];
const turretGeometry = new THREE.BoxGeometry(
turretWidth,
turretHeight,
turretLength
);
const turretMesh = new THREE.Mesh(turretGeometry, bodyMaterial);
turretMesh.castShadow = true;
const turretPivot = new THREE.Object3D();
turretPivot.scale.set(5, 5, 5);
turretPivot.position.y = 0.5;
turretPivot.position.z = turretLength * 0.5;
turretPivot.add(turretMesh);
bodyMesh.add(turretPivot);
const turretCamera = makeCamera();
turretCamera.position.y = 0.75 * 0.2;
turretMesh.add(turretCamera);
const targetGeometry = new THREE.SphereGeometry(0.5, 6, 3);
const targetMaterial = new THREE.MeshPhongMaterial({
color: 0x00ff00,
flatShading: true,
});
const targetMesh = new THREE.Mesh(targetGeometry, targetMaterial);
targetMesh.castShadow = true;
const targetBob = new THREE.Object3D();
targetBob.add(targetMesh);
const targetElevation = new THREE.Object3D();
targetElevation.position.y = 8;
targetElevation.position.z = carLength * 2;
targetElevation.add(targetBob);
const targetOrbit = new THREE.Object3D();
targetOrbit.add(targetElevation);
scene.add(targetOrbit);
const targetCamera = makeCamera();
targetCamera.position.set(0, 1, -2);
targetCamera.rotation.y = Math.PI;
const targetCameraPivot = new THREE.Object3D();
targetCameraPivot.add(targetCamera);
targetBob.add(targetCameraPivot);
const curve = new THREE.SplineCurve([
new THREE.Vector2(-10, 0),
new THREE.Vector2(-5, 5),
new THREE.Vector2(0, 0),
new THREE.Vector2(5, -5),
new THREE.Vector2(10, 0),
new THREE.Vector2(5, 10),
new THREE.Vector2(-5, 10),
new THREE.Vector2(-10, -10),
new THREE.Vector2(-15, -8),
new THREE.Vector2(-10, 0),
]);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
const splineObject = new THREE.Line(geometry, material);
splineObject.rotation.x = Math.PI * 0.5;
splineObject.position.y = 0.05;
scene.add(splineObject);
const targetPosition = new THREE.Vector3();
const tankPosition = new THREE.Vector2();
const tankTarget = new THREE.Vector2();
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
let needResize = width !== canvas.width || height !== canvas.height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
const infoElem = document.createElement("div");
document.body.appendChild(infoElem);
const cameras = [
{ cam: camera, desc: "detached camera" },
{ cam: turretCamera, desc: "on turret looking at target" },
{ cam: targetCamera, desc: "near target looking at tank" },
{ cam: tankCamera, desc: "above back of tank" },
];
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
cameras.forEach((cameraInfo) => {
const camera = cameraInfo.cam;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
});
}
const tankTime = time * 0.05;
curve.getPointAt(tankTime % 1, tankPosition);
curve.getPointAt((tankTime + 0.01) % 1, tankTarget);
tank.position.set(tankPosition.x, 0, tankPosition.y);
tank.lookAt(tankTarget.x, 0, tankTarget.y);
targetOrbit.rotation.y = time * 0.27;
targetBob.position.y = Math.sin(time * 2) * 4;
targetMesh.rotation.x = time * 7;
targetMesh.rotation.y = time * 13;
targetMaterial.emissive.setHSL((time * 10) % 1, 1, 0.25);
targetMaterial.color.setHSL((time * 10) % 1, 1, 0.25);
targetMesh.getWorldPosition(targetPosition);
turretPivot.lookAt(targetPosition);
turretCamera.lookAt(targetPosition);
tank.getWorldPosition(targetPosition);
targetCameraPivot.lookAt(targetPosition);
const camera = cameras[(time * 0.25) % cameras.length | 0];
infoElem.textContent = camera.desc;
renderer.render(scene, camera.cam);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
材质
MeshBasicMaterial
不受光照影响
MeshLambertMaterial
只在顶点计算光照
MeshPhongMaterial
在每个像素计算光照,支持镜面高光shininess
MeshToonMaterial
类似MeshPhongMaterial
,使用渐变图着色。
MeshStandardMaterial
相比MeshPhongMaterial
它通过roughness和metalness 设置粗糙度和金属度
MeshPhysicalMaterial
相比MeshStandardMaterial
增加了clearcoat表示透明涂层的厚度,clearCoatRoughness 表示透明涂层的粗糙度
shadowMaterial
用于接收阴影
MeshDepthMaterial
渲染每个像素的深度
MeshNormalMaterial
显示几何体的法线
ShaderMaterial
通过着色器自定义材质
RawShaderMaterial
制作完全自定义的着色器
有些材质设置需要手动更新材质变化material.needsUpdate
。
例如修改flatShading,添加删除纹理等等。(删除纹理改成使用1*1像素的白色纹理更好)。
纹理
- 加载纹理
const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/flower-1.jpg');
-
不等待纹理加载完成,可能会出现下面的情况。优点是会立即开始渲染。可以通过回调保证加载完成
loaderload('img',(texture)=>{})
等待多纹理加载可以使用
loadManager
const loadManager = new THREE.LoadingManager();
const loader = new THREE.TextureLoader(loadManager);
const materials = [
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
];
// 完成回调
loadManager.onLoad = () => {
const cube = new THREE.Mesh(geometry, materials);
scene.add(cube);
};
// 进度回调
loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
// todo
};
- 纹理会占用
宽度*高度*4*1.33
字节的内存。所以尽可能文件小且尺寸小。 - PNG支持透明度,可以非图像数据的格式,比如法线图等。
-
magFilter
当纹理绘制的尺寸大于其原始尺寸时
-
NearestFilter
从原始纹理中选取最接近的一个像素 -
LinearFilter
从纹理中选择离我们应该选择颜色最近的4个像素,并根据实际点与4个像素的距离,以适当的比例进行混合。
-
minFilter
在绘制纹理的尺寸小于其原始尺寸时
-
NearestFilter
在纹理中选择最近的像素。 -
LinearFilter
从纹理中选择4个像素,然后混合它们 -
NearestMipmapNearestFilter
选择最近的mip,然后选择一个像素。 -
NearestMipmapLinearFilter
选择2个mips,从每个mips中选择一个像素,混合这2个像素。 -
LinearMipmapNearestFilter
选择合适的mip,然后选择4个像素并将它们混合。 -
LinearMipmapLinearFilter
选择2个mips,从每个mips中选择4个像素,然后将所有8个像素混合成1个像素。
- 纹理重复
wrapS
和wrapT
-
ClampToEdgeWrapping
每条边上的最后一个像素无限重复。 -
RepeatWrapping
纹理重复 -
MirroredRepeatWrapping
在每次重复时将进行镜像
- 纹理偏移 offset
- 纹理旋转
//旋转中心
someTexture.center.set(.5, .5);
//旋转弧度
someTexture.rotation = THREE.MathUtils.degToRad(45);
import * as THREE from "../../three/build/three";
import * as dat from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xaaaaaa);
const fov = 75;
const aspect = 2;
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load("../assets/images/wall.jpg");
const material = new THREE.MeshBasicMaterial({
map: texture,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
class DegRadHelper {
constructor(obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return THREE.MathUtils.radToDeg(this.obj[this.prop]);
}
set value(v) {
this.obj[this.prop] = THREE.MathUtils.degToRad(v);
}
}
class StringToNumberHelper {
constructor(obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return this.obj[this.prop];
}
set value(v) {
this.obj[this.prop] = parseFloat(v);
}
}
const wrapModes = {
ClampToEdgeWrapping: THREE.ClampToEdgeWrapping,
RepeatWrapping: THREE.RepeatWrapping,
MirroredRepeatWrapping: THREE.MirroredRepeatWrapping,
};
function updateTexture() {
texture.needsUpdate = true;
}
const gui = new dat.GUI();
gui
.add(new StringToNumberHelper(texture, "wrapS"), "value", wrapModes)
.name("texture.wrapS")
.onChange(updateTexture);
gui
.add(new StringToNumberHelper(texture, "wrapT"), "value", wrapModes)
.name("texture.wrapT")
.onChange(updateTexture);
gui.add(texture.repeat, "x", 0, 5, 0.01).name("texture.repeat.x");
gui.add(texture.repeat, "y", 0, 5, 0.01).name("texture.repeat.y");
gui.add(texture.offset, "x", -2, 2, 0.01).name("texture.offset.x");
gui.add(texture.offset, "y", -2, 2, 0.01).name("texture.offset.y");
gui.add(texture.center, "x", -0.5, 1.5, 0.01).name("texture.center.x");
gui.add(texture.center, "y", -0.5, 1.5, 0.01).name("texture.center.y");
gui
.add(new DegRadHelper(texture, "rotation"), "value", -360, 360)
.name("texture.rotation");
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cube.rotation.x += 0.005;
cube.rotation.y += 0.005;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
光照
环境光AmbientLight
简单地将材质的颜色与光照颜色进行叠加,再乘以光照强度。它没有方向,无法产生阴影,场景内任何一点受到的光照强度都是相同的,除了改变场景内所有物体的颜色以外,不会使物体产生明暗的变化,看起来并不像真正意义上的光照。通常的作用是提亮场景,让暗部不要太暗。
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.AmbientLight(color, intensity);
scene.add(light);
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "intensity", 0, 2, 0.01);
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
半球光HemisphereLight
半球光HemisphereLight的颜色是从天空到地面两个颜色的渐变,与物体材质的颜色叠加后得到最终的颜色。一个点受到的光照颜色是由所在平面的朝向(法向量)决定的 —— 面向正上方就受到天空的光照颜色,面向正下方就受到地面的光照颜色,其他角度则是两个颜色渐变区间的颜色。
HemisphereLight
与其他类型光照结合使用,可以很好地表现天空和地面颜色照射到物体上的效果。所以最好的使用场景就是与其他光照结合使用。
{
const skyColor = 0xb1e1ff;
const groundColor = 0xb97a20;
const intensity = 1;
const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("skyColor");
gui
.addColor(new ColorGUIHelper(light, "groundColor"), "value")
.name("groundColor");
gui.add(light, "intensity", 0, 2, 0.01);
}
方向光DirectionalLight
常常用来表现太阳光照的效果。
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
{
function makeXYZGUI(gui, vector3, name, onChangeFn) {
const folder = gui.addFolder(name);
folder.add(vector3, "x", -10, 10).onChange(onChangeFn);
folder.add(vector3, "y", 0, 10).onChange(onChangeFn);
folder.add(vector3, "z", -10, 10).onChange(onChangeFn);
folder.open();
}
function updateLight() {
light.target.updateMatrixWorld();
helper.update();
}
const color = 0xffffff;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "intensity", 0, 2, 0.01);
gui.add(light.target.position, "x", -10, 10);
gui.add(light.target.position, "z", -10, 10);
gui.add(light.target.position, "y", 0, 10);
makeXYZGUI(gui, light.position, "position", updateLight);
makeXYZGUI(gui, light.target.position, "target", updateLight);
const helper = new THREE.DirectionalLightHelper(light);
scene.add(helper);
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
点光源PointLight
表示的是从一个点朝各个方向发射出光线的一种光照效果。
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.PointLight(color, intensity);
light.position.set(0, 10, 0);
scene.add(light);
const helper = new THREE.PointLightHelper(light);
scene.add(helper);
function updateLight() {
helper.update();
}
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "intensity", 0, 2, 0.01);
gui.add(light, "distance", 0, 40).onChange(updateLight);
makeXYZGUI(gui, light.position, "position", updateLight);
function makeXYZGUI(gui, vector3, name, onChangeFn) {
const folder = gui.addFolder(name);
folder.add(vector3, "x", -10, 10).onChange(onChangeFn);
folder.add(vector3, "y", 0, 10).onChange(onChangeFn);
folder.add(vector3, "z", -10, 10).onChange(onChangeFn);
folder.open();
}
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
聚光灯SpotLight
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
class DegRadHelper {
constructor(obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return THREE.MathUtils.radToDeg(this.obj[this.prop]);
}
set value(v) {
this.obj[this.prop] = THREE.MathUtils.degToRad(v);
}
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.SpotLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
const helper = new THREE.SpotLightHelper(light);
scene.add(helper);
function updateLight() {
light.target.updateMatrixWorld();
helper.update();
}
updateLight()
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "intensity", 0, 2, 0.01);
gui.add(light, "distance", 0, 40).onChange(updateLight);
gui
.add(new DegRadHelper(light, "angle"), "value", 0, 90)
.name("angle")
.onChange(updateLight);
gui.add(light, "penumbra", 0, 1, 0.01);
makeXYZGUI(gui, light.position, "position", updateLight);
makeXYZGUI(gui, light.target.position, "target", updateLight);
function makeXYZGUI(gui, vector3, name, onChangeFn) {
const folder = gui.addFolder(name);
folder.add(vector3, "x", -10, 10).onChange(onChangeFn);
folder.add(vector3, "y", 0, 10).onChange(onChangeFn);
folder.add(vector3, "z", -10, 10).onChange(onChangeFn);
folder.open();
}
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
矩形区域光RectAreaLight
表示一个矩形区域的发射出来的光照,例如长条的日光灯
RectAreaLight
只能影响MeshStandardMaterial
,MeshPhysicalMaterial
。
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
import { RectAreaLightUniformsLib } from "../../three/examples/jsm/lights/RectAreaLightUniformsLib";
import { RectAreaLightHelper } from "../../three/examples/jsm/helpers/RectAreaLightHelper";
RectAreaLightUniformsLib.init();
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
class DegRadHelper {
constructor(obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return THREE.MathUtils.radToDeg(this.obj[this.prop]);
}
set value(v) {
this.obj[this.prop] = THREE.MathUtils.degToRad(v);
}
}
{
const color = 0xffffff;
const intensity = 5;
const width = 12;
const height = 4;
const light = new THREE.RectAreaLight(color, intensity, width, height);
light.position.set(0, 10, 0);
light.rotation.x = THREE.MathUtils.degToRad(-90);
scene.add(light);
const helper = new RectAreaLightHelper(light);
light.add(helper);
function updateLight() {
helper.update();
}
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "intensity", 0, 10, 0.01);
gui.add(light, "width", 0, 20);
gui.add(light, "height", 0, 20);
gui
.add(new DegRadHelper(light.rotation, "x"), "value", -180, 180)
.name("x rotation");
gui
.add(new DegRadHelper(light.rotation, "y"), "value", -180, 180)
.name("y rotation");
gui
.add(new DegRadHelper(light.rotation, "z"), "value", -180, 180)
.name("z rotation");
makeXYZGUI(gui, light.position, "position");
function makeXYZGUI(gui, vector3, name, onChangeFn) {
const folder = gui.addFolder(name);
folder.add(vector3, "x", -10, 10).onChange(onChangeFn);
folder.add(vector3, "y", 0, 10).onChange(onChangeFn);
folder.add(vector3, "z", -10, 10).onChange(onChangeFn);
folder.open();
}
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshStandardMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshStandardMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshStandardMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
WebGLRenderer.physicallyCorrectLights
这个设置会影响点光源PointLight和聚光灯SpotLight,矩形区域光RectAreaLight会自动应用这个特性。
在设置光照时,不要设置 distance
来表现光照的衰减,也不要设置 intensity
。而是设置光照的 power
属性,以流明为单位,three.js 会进行物理计算,从而表现出接近真实的光照效果。在这种情况下 three.js 参与计算的长度单位是米,一个 60瓦 的灯泡大概是 800 流明强度。并且光源有一个 decay
属性,为了模拟真实效果,应该被设置为 2
。
import * as THREE from "../../three/build/three";
import { OrbitControls } from "../../three/examples/jsm/controls/OrbitControls";
import { GUI } from "dat.gui";
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.physicallyCorrectLights = true;
const scene = new THREE.Scene();
scene.background = new THREE.Color("black");
const [fov, aspect, near, far] = [45, 2, 0.1, 100];
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();
class ColorGUIHelper {
constructor(object, prop) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[this.prop].getHexString()}`;
}
set value(hexString) {
this.object[this.prop].set(hexString);
}
}
{
const color = 0xffffff;
const intensity = 1;
const light = new THREE.PointLight(color, intensity);
light.power = 800;
light.decay = 2;
light.distance = Infinity;
light.position.set(0, 10, 0);
scene.add(light);
const helper = new THREE.PointLightHelper(light);
scene.add(helper);
function updateLight() {
helper.update();
}
const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, "color"), "value").name("color");
gui.add(light, "decay", 0, 4, 0.01);
gui.add(light, "power", 0, 2000);
makeXYZGUI(gui, light.position, "position", updateLight);
function makeXYZGUI(gui, vector3, name, onChangeFn) {
const folder = gui.addFolder(name);
folder.add(vector3, "x", -10, 10).onChange(onChangeFn);
folder.add(vector3, "y", 0, 10).onChange(onChangeFn);
folder.add(vector3, "z", -10, 10).onChange(onChangeFn);
folder.open();
}
}
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load("./assets/images/checker.png");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -0.5;
scene.add(mesh);
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({ color: "#8ac" });
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(
sphereRadius,
sphereWidthDivisions,
sphereHeightDivisions
);
const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
最后
每添加一个光源到场景中,都会降低 three.js 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果。