three模拟的真实3D环境,一个非常炫酷的功能便是对光源的操控,之前教学一中已经简单的描述了多种光源,这次咱们就详细的讲下一些最常见的光源:
AmbientLight
该灯光在全局范围内平等地照亮场景中的所有对象。
该灯光不能用于投射阴影,因为它没有方向。
AmbientLight( color : Integer, intensity : Float )
color - (可选)颜色的RGB分量的数值。默认值为0xffffff。
intensity - (可选)灯光强度/强度的数值。默认值为1。
DirectionalLight
向特定方向发射的光。这种光的行为就像它是无限遥远的,并且从它产生的光线都是平行的。这方面的常见用例是模拟日光;太阳离得足够远,它的位置可以被认为是无限的,所有来自它的光线都是平行的。
DirectionalLight( color : Integer, intensity : Float )
color - (可选)灯光的十六进制颜色。默认值为0xffffff(白色)。
intensity - (可选)灯光强度/强度的数值。默认值为1。
PointLight
从一个点向所有方向发射的光。这方面的一个常见用例是复制裸灯泡发出的光。
PointLight( color : Integer, intensity : Float, distance : Number, decay : Float )
color - (可选)灯光的十六进制颜色。默认值为0xffffff(白色)
intensity - (可选)灯光强度/强度的数值。默认值为1.
distance - 光线的最大范围。默认值为0(无限制)
decay - 灯光沿着灯光的距离变暗的量。默认值为2.
HemisphereLight
位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。此灯光不能用于投射阴影。
HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )
skyColor - (可选)天空的十六进制颜色。默认为0xffffff。
groundColor - (可选)地球的十六进制颜色。默认为0xffffff。
intensity - 可选)光的强度/强度的数值。默认为1。
SpotLight
这种光从一个方向的单个点发射,沿着一个圆锥体,该圆锥体的大小随着光的传播而增加。类似车灯的效果,可以做射灯、车灯等
SpotLight( color : Integer, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float )
color -(可选)灯光的十六进制颜色。默认值为0xffffff(白色)。
intensity -(可选)灯光强度/强度的数值。默认值为1。
distance - 灯光的最大范围。默认值为0(无限制)。
angle - 光从其方向散射的最大角度,其上限为Math。π/2。
penumbra - 由于半影而衰减的聚光灯圆锥体的百分比。取值介于0和1之间。默认值为零。
decay - 光线沿着光线的距离变暗的量。
看了定义后,我们先看下本次案例的效果图
这个案例中涉及到了以上所有光源效果,其中DirectionalLight和HemisphereLight是注释掉了的,为的是突出PointLight 和 SpotLight的效果,在代码中会发现AmbientLight的灯光强度我也特意变暗了。
案例中首次使用了GUI操作器和物体阴影效果。
GUI主要作用:
1、获取一个对象和该对象上的属性名,并根据属性的类型自动生成一个界面组件来操作该属性。
2、使用它后,我们可以通过界面组件来控制场景中的物体,提高调试效率。
看代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body {
width: 100%;
height: 100%;
}
* {
margin: 0;
padding: 0;
}
.label {
font-size: 20px;
color: #000;
font-weight: 700;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "../three-155/build/three.module.js",
"three/addons/": "../three-155/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {GPUStatsPanel} from 'three/addons/utils/GPUStatsPanel.js';
import {CSS2DRenderer, CSS2DObject} from 'three/addons/renderers/CSS2DRenderer.js';
import {PLYLoader} from 'three/addons/loaders/PLYLoader.js';
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
let stats, labelRenderer, gpuPanel, curve, lightHelper, textures;
let camera, scene, renderer, target, controls, lights, spotLight;
const group = new THREE.Group();
let progress = 0; // 物体运动时在运动路径的初始位置,范围0~1
const velocity = 0.0005; // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率
let widthImg = 200;
let heightImg = 200;
let time = 0;
init();
initHelp();
initLight();
axesHelperWord();
animate();
// 添加平面
addPlane();
lights = initPointLight(20, 1);
initSpotLight();
addModel();
addGUI();
function initPointLight(lightNum, lightR) {
let vector = new THREE.Vector3();
let geometry = new THREE.SphereGeometry(lightR, 15, 15);
let lights = [];
for (let i = 0; i < lightNum; i++) {
let pointLight = new THREE.PointLight(0xff0000, 150, 30, 2);
vector.set(Math.random(), Math.random(), Math.random()).normalize();
pointLight.color.setRGB(vector.x, vector.y, vector.z);
scene.add(pointLight);
lights.push(pointLight);
let material = new THREE.MeshBasicMaterial({color: pointLight.color});
let emitter = new THREE.Mesh(geometry, material);
pointLight.add(emitter);
}
return lights;
}
function initSpotLight() {
// 位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。此灯光不能用于投射阴影。
// const ambient = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 0.15 );
// scene.add( ambient );
const loader = new THREE.TextureLoader().setPath('../js_three-0.108.0/examples/textures/');
const filenames = ['disturb.jpg', 'colors.png', 'uv_grid_opengl.jpg'];
textures = {none: null};
for (let i = 0; i < filenames.length; i++) {
const filename = filenames[i];
const texture = loader.load(filename);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.colorSpace = THREE.SRGBColorSpace;
textures[filename] = texture;
}
const targetObject = new THREE.Object3D();
targetObject.position.set(-50, 0, -50);
scene.add(targetObject);
spotLight = new THREE.SpotLight(0xffffff, 100);
spotLight.position.set(-50, 50, -50);
spotLight.target = targetObject;
spotLight.angle = Math.PI / 4;
spotLight.intensity = 10000;
spotLight.penumbra = 1;
spotLight.decay = 1.5;
spotLight.distance = 300;
spotLight.map = textures['uv_grid_opengl.jpg'];
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 200;
spotLight.shadow.focus = 1;
scene.add(spotLight);
lightHelper = new THREE.SpotLightHelper(spotLight);
scene.add(lightHelper);
}
function addModel() {
new PLYLoader().load('../js_three-0.108.0/examples/models/ply/binary/Lucy100k.ply', function (geometry) {
let scale = 0.02;
geometry.scale(scale, scale, scale);
geometry.computeVertexNormals();
const material = new THREE.MeshLambertMaterial();
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.y = -Math.PI / 2;
mesh.position.y = 12;
mesh.position.x = -50;
mesh.position.z = -50;
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
});
}
function addGUI() {
// GUI
const gui = new GUI();
const params = {
map: textures['disturb.jpg'],
color: spotLight.color.getHex(),
intensity: spotLight.intensity,
distance: spotLight.distance,
angle: spotLight.angle,
penumbra: spotLight.penumbra,
decay: spotLight.decay,
focus: spotLight.shadow.focus,
shadows: true
};
gui.add(params, 'map', textures).onChange(function (val) {
spotLight.map = val;
});
gui.addColor(params, 'color').onChange(function (val) {
spotLight.color.setHex(val);
});
gui.add(params, 'intensity', 0, 20000).onChange(function (val) {
spotLight.intensity = val;
});
gui.add(params, 'distance', 50, 200).onChange(function (val) {
spotLight.distance = val;
});
gui.add(params, 'angle', 0, Math.PI / 3).onChange(function (val) {
spotLight.angle = val;
});
gui.add(params, 'penumbra', 0, 1).onChange(function (val) {
spotLight.penumbra = val;
});
gui.add(params, 'decay', 1, 2).onChange(function (val) {
spotLight.decay = val;
});
gui.add(params, 'focus', 0, 1).onChange(function (val) {
spotLight.shadow.focus = val;
});
gui.add(params, 'shadows').onChange(function (val) {
renderer.shadowMap.enabled = val;
scene.traverse(function (child) {
if (child.material) {
child.material.needsUpdate = true;
}
});
});
gui.open();
}
function addPlane() {
// 创建一个平面 PlaneGeometry(width, height, widthSegments, heightSegments)
const planeGeometry = new THREE.PlaneGeometry(widthImg, heightImg, 1, 1);
// 创建 Lambert 材质:会对场景中的光源作出反应,但表现为暗淡,而不光亮。
const planeMaterial = new THREE.MeshPhongMaterial({
color: 0xb2d3e6,
side: THREE.DoubleSide
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 以自身中心为旋转轴,绕 x 轴顺时针旋转 45 度
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, -4, 0);
plane.castShadow = true;
plane.receiveShadow = true;
scene.add(plane);
}
function init() {
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 10, 2000);
camera.up.set(0, 1, 0);
camera.position.set(60, 40, 60);
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
scene.background = new THREE.Color('#ccc');
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 色调映射的曝光级别
renderer.toneMappingExposure = 1;
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none';
document.getElementById('container').appendChild(labelRenderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE
};
controls.enablePan = true;
// 设置最大最小视距
controls.minDistance = 5;
controls.maxDistance = 1200;
window.addEventListener('resize', onWindowResize);
stats = new Stats();
stats.setMode(1); // 0: fps, 1: ms
document.body.appendChild(stats.dom);
gpuPanel = new GPUStatsPanel(renderer.getContext());
stats.addPanel(gpuPanel);
stats.showPanel(0);
scene.add(group);
}
function initLight() {
// const light = new THREE.DirectionalLight(new THREE.Color('rgb(253,253,253)'));
// light.position.set(10, 10, 1);
// light.intensity = 3; // 光线强度
const AmbientLight = new THREE.AmbientLight(new THREE.Color('rgb(255, 255, 255)'), 0.3);
// scene.add( light );
scene.add(AmbientLight);
}
function initHelp() {
// const size = 100;
// const divisions = 5;
// const gridHelper = new THREE.GridHelper( size, divisions );
// scene.add( gridHelper );
// The X axis is red. The Y axis is green. The Z axis is blue.
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
}
function axesHelperWord() {
let xP = addWord('X轴');
let yP = addWord('Y轴');
let zP = addWord('Z轴');
xP.position.set(50, 0, 0);
yP.position.set(0, 50, 0);
zP.position.set(0, 0, 50);
}
function addWord(word) {
let name = `<span>${word}</span>`;
let moonDiv = document.createElement('div');
moonDiv.className = 'label';
// moonDiv.textContent = 'Moon';
// moonDiv.style.marginTop = '-1em';
moonDiv.innerHTML = name;
const label = new CSS2DObject(moonDiv);
group.add(label);
return label;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
time += 0.05;
if (lights) {
let swordTime = time * 0.1;
let dis = 60;
for (let i = 0, il = lights.length; i < il; i++) {
let light = lights[i];
let x = Math.sin(swordTime + i * 7.0) * dis;
let y = Math.cos(swordTime + i * 5.0) * dis;
let z = Math.cos(swordTime + i * 3.0) * dis;
light.position.set(x, y, z);
}
}
if (lightHelper) {
lightHelper.update();
const time = performance.now() / 2000;
spotLight.position.x = Math.cos(time) * 2 - 50;
spotLight.position.z = Math.sin(time) * 2 - 50;
}
stats.update();
controls.update();
if (labelRenderer) {
labelRenderer.render(scene, camera);
}
renderer.render(scene, camera);
}
</script>
</body>
</html>
关于shadow效果的设置我们需要特别注意:
1、我们需要首先在renderer中设置
2、需要阴影的物体需要设置
3、这一点是比较容易遗忘的,那就是物体通过光源产生的阴影 投射在哪,相应的物体也需要设置,这个点写这个案例时,我也忘了 折腾半天模型的投影看不到,突然想到我addPlane函数创建的平面没有设置,简直了,耗费了很长时间
当然案例中只是shadow的一种设置方式,后面会讲到别的方式。
案例中还有一个知识点,之所以我将人像模型放在边缘,也是为了这个。那就是spotLight光源默认指向中心点也就是 (0, 0, 0)点,这时我们还需要spotLight实现案例中的效果,必须改变spotLight的指向:
const targetObject = new THREE.Object3D();
targetObject.position.set(-50, 0, -50);
scene.add(targetObject);
// 这样就可以了
spotLight.target = targetObject;