threejs-day2(场景/材质/纹理/光照)

场景

主要就是父子关系坐标系关系。直接上例子

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

材质

MeshBasicMaterial 不受光照影响
MeshLambertMaterial 只在顶点计算光照
MeshPhongMaterial在每个像素计算光照,支持镜面高光shininess
MeshToonMaterial 类似MeshPhongMaterial,使用渐变图着色。
MeshStandardMaterial相比MeshPhongMaterial它通过roughness和metalness 设置粗糙度和金属度
MeshPhysicalMaterial相比MeshStandardMaterial增加了clearcoat表示透明涂层的厚度,clearCoatRoughness 表示透明涂层的粗糙度
shadowMaterial用于接收阴影
MeshDepthMaterial渲染每个像素的深度
MeshNormalMaterial显示几何体的法线
ShaderMaterial通过着色器自定义材质
RawShaderMaterial制作完全自定义的着色器

有些材质设置需要手动更新材质变化material.needsUpdate
例如修改flatShading,添加删除纹理等等。(删除纹理改成使用1*1像素的白色纹理更好)。

纹理

  1. 加载纹理
const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/flower-1.jpg');
  1. 不等待纹理加载完成,可能会出现下面的情况。优点是会立即开始渲染。可以通过回调保证加载完成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')}),
];
 // 完成回调
loadManager.onLoad = () => {
  const cube = new THREE.Mesh(geometry, materials);
  scene.add(cube);
};
// 进度回调
loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
// todo
};
  1. 纹理会占用 宽度*高度*4*1.33字节的内存。所以尽可能文件小且尺寸小。
  2. PNG支持透明度,可以非图像数据的格式,比如法线图等。
  3. magFilter当纹理绘制的尺寸大于其原始尺寸时
  • NearestFilter 从原始纹理中选取最接近的一个像素
  • LinearFilter 从纹理中选择离我们应该选择颜色最近的4个像素,并根据实际点与4个像素的距离,以适当的比例进行混合。
  1. minFilter 在绘制纹理的尺寸小于其原始尺寸时
  • NearestFilter 在纹理中选择最近的像素。
  • LinearFilter 从纹理中选择4个像素,然后混合它们
  • NearestMipmapNearestFilter 选择最近的mip,然后选择一个像素。
  • NearestMipmapLinearFilter选择2个mips,从每个mips中选择一个像素,混合这2个像素。
  • LinearMipmapNearestFilter选择合适的mip,然后选择4个像素并将它们混合。
  • LinearMipmapLinearFilter选择2个mips,从每个mips中选择4个像素,然后将所有8个像素混合成1个像素。
  1. 纹理重复 wrapSwrapT
  • ClampToEdgeWrapping 每条边上的最后一个像素无限重复。
  • RepeatWrapping纹理重复
  • MirroredRepeatWrapping在每次重复时将进行镜像
  1. 纹理偏移 offset
  2. 纹理旋转
//旋转中心
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 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容