threejs-day2(場景/材質(zhì)/紋理/光照)

場景

主要就是父子關(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);

動畫.gif

材質(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像素的白色紋理更好)。

紋理

  1. 加載紋理
const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/flower-1.jpg');
  1. 不等待紋理加載完成,可能會出現(xiàn)下面的情況。優(yōu)點是會立即開始渲染??梢酝ㄟ^回調(diào)保證加載完成loaderload('img',(texture)=>{})

    動畫1.gif

  2. 等待多紋理加載可以使用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
};
  1. 紋理會占用 寬度*高度*4*1.33字節(jié)的內(nèi)存。所以盡可能文件小且尺寸小。
  2. PNG支持透明度,可以非圖像數(shù)據(jù)的格式,比如法線圖等。
  3. magFilter當(dāng)紋理繪制的尺寸大于其原始尺寸時
  • NearestFilter 從原始紋理中選取最接近的一個像素
  • LinearFilter 從紋理中選擇離我們應(yīng)該選擇顏色最近的4個像素,并根據(jù)實際點與4個像素的距離,以適當(dāng)?shù)谋壤M(jìn)行混合。
  1. minFilter 在繪制紋理的尺寸小于其原始尺寸時
  • NearestFilter 在紋理中選擇最近的像素。
  • LinearFilter 從紋理中選擇4個像素,然后混合它們
  • NearestMipmapNearestFilter 選擇最近的mip,然后選擇一個像素。
  • NearestMipmapLinearFilter選擇2個mips,從每個mips中選擇一個像素,混合這2個像素。
  • LinearMipmapNearestFilter選擇合適的mip,然后選擇4個像素并將它們混合。
  • LinearMipmapLinearFilter選擇2個mips,從每個mips中選擇4個像素,然后將所有8個像素混合成1個像素。
  1. 紋理重復(fù) wrapSwrapT
  • ClampToEdgeWrapping 每條邊上的最后一個像素?zé)o限重復(fù)。
  • RepeatWrapping紋理重復(fù)
  • MirroredRepeatWrapping在每次重復(fù)時將進(jìn)行鏡像
  1. 紋理偏移 offset
  2. 紋理旋轉(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)想要的效果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容