場景
主要就是父子關(guān)系坐標(biāo)系關(guān)系。直接上例子
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);

材質(zhì)
MeshBasicMaterial 不受光照影響
MeshLambertMaterial 只在頂點計算光照
MeshPhongMaterial在每個像素計算光照,支持鏡面高光shininess
MeshToonMaterial 類似MeshPhongMaterial,使用漸變圖著色。
MeshStandardMaterial相比MeshPhongMaterial它通過roughness和metalness 設(shè)置粗糙度和金屬度
MeshPhysicalMaterial相比MeshStandardMaterial增加了clearcoat表示透明涂層的厚度,clearCoatRoughness 表示透明涂層的粗糙度
shadowMaterial用于接收陰影
MeshDepthMaterial渲染每個像素的深度
MeshNormalMaterial顯示幾何體的法線
ShaderMaterial通過著色器自定義材質(zhì)
RawShaderMaterial制作完全自定義的著色器
有些材質(zhì)設(shè)置需要手動更新材質(zhì)變化material.needsUpdate。
例如修改flatShading,添加刪除紋理等等。(刪除紋理改成使用1*1像素的白色紋理更好)。
紋理
- 加載紋理
const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/flower-1.jpg');
-
不等待紋理加載完成,可能會出現(xiàn)下面的情況。優(yōu)點是會立即開始渲染??梢酝ㄟ^回調(diào)保證加載完成
loaderload('img',(texture)=>{})
動畫1.gif 等待多紋理加載可以使用
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')}),
];
// 完成回調(diào)
loadManager.onLoad = () => {
const cube = new THREE.Mesh(geometry, materials);
scene.add(cube);
};
// 進(jìn)度回調(diào)
loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
// todo
};
- 紋理會占用
寬度*高度*4*1.33字節(jié)的內(nèi)存。所以盡可能文件小且尺寸小。 - PNG支持透明度,可以非圖像數(shù)據(jù)的格式,比如法線圖等。
-
magFilter當(dāng)紋理繪制的尺寸大于其原始尺寸時
-
NearestFilter從原始紋理中選取最接近的一個像素 -
LinearFilter從紋理中選擇離我們應(yīng)該選擇顏色最近的4個像素,并根據(jù)實際點與4個像素的距離,以適當(dāng)?shù)谋壤M(jìn)行混合。
-
minFilter在繪制紋理的尺寸小于其原始尺寸時
-
NearestFilter在紋理中選擇最近的像素。 -
LinearFilter從紋理中選擇4個像素,然后混合它們 -
NearestMipmapNearestFilter選擇最近的mip,然后選擇一個像素。 -
NearestMipmapLinearFilter選擇2個mips,從每個mips中選擇一個像素,混合這2個像素。 -
LinearMipmapNearestFilter選擇合適的mip,然后選擇4個像素并將它們混合。 -
LinearMipmapLinearFilter選擇2個mips,從每個mips中選擇4個像素,然后將所有8個像素混合成1個像素。
- 紋理重復(fù)
wrapS和wrapT
-
ClampToEdgeWrapping每條邊上的最后一個像素?zé)o限重復(fù)。 -
RepeatWrapping紋理重復(fù) -
MirroredRepeatWrapping在每次重復(fù)時將進(jìn)行鏡像
- 紋理偏移 offset
- 紋理旋轉(zhuǎn)
//旋轉(zhuǎn)中心
someTexture.center.set(.5, .5);
//旋轉(zhuǎn)弧度
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);
光照
環(huán)境光AmbientLight
簡單地將材質(zhì)的顏色與光照顏色進(jìn)行疊加,再乘以光照強(qiáng)度。它沒有方向,無法產(chǎn)生陰影,場景內(nèi)任何一點受到的光照強(qiáng)度都是相同的,除了改變場景內(nèi)所有物體的顏色以外,不會使物體產(chǎn)生明暗的變化,看起來并不像真正意義上的光照。通常的作用是提亮場景,讓暗部不要太暗。
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的顏色是從天空到地面兩個顏色的漸變,與物體材質(zhì)的顏色疊加后得到最終的顏色。一個點受到的光照顏色是由所在平面的朝向(法向量)決定的 —— 面向正上方就受到天空的光照顏色,面向正下方就受到地面的光照顏色,其他角度則是兩個顏色漸變區(qū)間的顏色。
HemisphereLight與其他類型光照結(jié)合使用,可以很好地表現(xiàn)天空和地面顏色照射到物體上的效果。所以最好的使用場景就是與其他光照結(jié)合使用。
{
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
常常用來表現(xiàn)太陽光照的效果。
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
表示的是從一個點朝各個方向發(fā)射出光線的一種光照效果。
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);
矩形區(qū)域光RectAreaLight
表示一個矩形區(qū)域的發(fā)射出來的光照,例如長條的日光燈
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
這個設(shè)置會影響點光源PointLight和聚光燈SpotLight,矩形區(qū)域光RectAreaLight會自動應(yīng)用這個特性。
在設(shè)置光照時,不要設(shè)置 distance 來表現(xiàn)光照的衰減,也不要設(shè)置 intensity。而是設(shè)置光照的 power屬性,以流明為單位,three.js 會進(jìn)行物理計算,從而表現(xiàn)出接近真實的光照效果。在這種情況下 three.js 參與計算的長度單位是米,一個 60瓦 的燈泡大概是 800 流明強(qiáng)度。并且光源有一個 decay 屬性,為了模擬真實效果,應(yīng)該被設(shè)置為 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 渲染場景的速度,所以應(yīng)該盡量使用最少的資源來實現(xiàn)想要的效果。
