three.js 示例学习 | 03-光线与物体的作用 lights_physical

644 阅读9分钟

前言

本篇我们看一下three.js官方示例的webgl_lights_physical 了解一下3D 中光线和物体的之间的作用。

实现

import * as THREE from 'three';

let camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,1,10000);
camera.position.set( -4, 2, 4 );
camera.lookAt(0,0,0);

let scene = new THREE.Scene();

let renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

添加基本场景结构

const bulbGeometry = new THREE.SphereGeometry(0.02, 16, 8);
let bulbLight = new THREE.PointLight(0xffee88, 1, 100, 2);
let bulbMat = new THREE.MeshStandardMaterial({
    emissive: 0xffffee,
    emissiveIntensity: 1,
    color: 0x000000,
})
bulbLight.add(new THREE.Mesh(bulbGeometry, bulbMat));
bulbLight.position.set(0,2,0);
bulbLight.castShadow = true;
scene.add(bulbLight);

创建灯泡物体并加入到场景中。

SphereGeometry 是 用于生成球形几何体。构造参数:

属性描述
radius球体半径。
widthSegments水平分段数(沿着经线分段)。
heightSegments垂直分段数(沿着纬线分段)。

widthSegments 和 heightSegments 的分段数越多,球体的表面越光滑。

PointLight 是点光源,常用于模拟灯泡发出的光。构造参数:

属性描述
color十六进制光照颜色。
intensity光照强度。
distance这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。
decay沿着光照距离的衰退量。

MeshStandardMaterial 是一种基于物理的标准网格材质。

属性描述
emissive材质的放射(光)颜色,基本上是不受其他光照影响的固有颜色
emissiveIntensity放射光强度。调节发光颜色。默认为1.
color材质的颜色。

threejs提供了很多常用的材质效果,其他常用到的材质有:

材质类型描述
MeshBasicMaterial基础网格材质,不受光照影响的材质
MeshLambertMaterialLambert网格材质,与光照有反应,漫反射
MeshPhongMaterial高光Phong材质,与光照有反应
MeshStandardMaterialPBR物理材质,相比较高光Phong材质可以更好的模拟金属、玻璃等效果

bulbLight 是只有光线作用,3D中并没有显示。我们将bulbGeometry 和 bulbMat 组成mesh 添加到 bulbLight 中,这样这个灯泡在3D中就有属于它的样子了。

bulbLight 上的 castShadow 属性是光线是否与物体发生投影计算。默认是false。想要照射出投影,被照射到的物体上的 castShadow 属性也需要设置为true

const hemiLight = new THREE.HemisphereLight(0xddeeff, 0x0f0e0d, 0.02);
scene.add(hemiLight);

HemisphereLight 是半球光光源。这种光源可以为室外场景创建更加贴近自然的光照效果。它的构造参数为:

属性描述
skyColor天空中发出光线的颜色。缺省值 0xffffff。
groundColor地面发出光线的颜色。缺省值 0xffffff。
intensity光照强度。 缺省值 1。

半球光的光线示意图如下:

1657956373443.jpg

const floorMat = new THREE.MeshStandardMaterial( {
    roughness: 0.8,
    color: 0xffffff,
    metalness: 0.2,
    bumpScale: 0.0005
} );

MeshStandardMaterial是一种基于物理的标准网格材质。

属性描述
roughness材质的粗糙程度。0.0表示平滑的镜面反射,1.0表示完全漫反射。默认值为1.0。如果还提供roughnessMap,则两个值相乘。
metalness材质与金属的相似度。非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值。 默认值为0.0。0.0到1.0之间的值可用于生锈金属的外观。如果还提供了metalnessMap,则两个值相乘。
bumpScale凹凸贴图会对材质产生多大影响。典型范围是0-1。默认值为1。
map颜色贴图。
bumpMap用于创建凹凸贴图的纹理。黑色和白色值映射到与光照相关的感知深度。凹凸实际上不会影响对象的几何形状,只影响光照。如果定义了法线贴图,则将忽略该贴图。
roughnessMap该纹理的绿色通道用于改变材质的粗糙度。
needsUpdate表示下一帧需要更新编译材质。

bumpMap 和roughnessMap 用来表现材质表面不同的深度的凹凸和粗糙程度,使物体更有真实感。

如下代码,纹理加载器load 纹理贴图是一个异步的过程 所以在加载完成并赋值后需要设置needsUpdate 更新。

import hardwood2_diffuse from '../../asset/lights/hardwood2_diffuse.jpg';
import hardwood2_bump from '../../asset/lights/hardwood2_bump.jpg';
import hardwood2_roughness from '../../asset/lights/hardwood2_roughness.jpg';

const textureLoader = new THREE.TextureLoader();
textureLoader.load(hardwood2_diffuse, function(map) {
    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    map.anisotropy = 4;
    map.repeat.set(10, 24);
    map.encoding = THREE.sRGBEncoding;
    floorMat.map = map;
    floorMat.needsUpdate = true;
})

textureLoader.load( hardwood2_bump, function ( map ) {

    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    map.anisotropy = 4;
    map.repeat.set( 10, 24 );
    floorMat.bumpMap = map;
    floorMat.needsUpdate = true;
} );
textureLoader.load( hardwood2_roughness, function ( map ) {

    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    map.anisotropy = 4;
    map.repeat.set( 10, 24 );
    floorMat.roughnessMap = map;
    floorMat.needsUpdate = true;
} );

TextureLoader(纹理加载器)。

.load ( url : String, onLoad : Function, onProgress : Function, onError : Function ) : Texture

属性描述
url文件的URL或者路径
onLoad加载完成时将调用。回调参数为将要加载的texture.
onProgress将在加载过程中进行调用。参数为XMLHttpRequest实例
onError在加载错误时被调用。

被加载的纹理属于Texture 类型。

属性描述
wrapS这个值定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于U。
wrapT这个值定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于V。
anisotropy默认1,值越高结果越模糊,使用的纹理样本就越多,通常为2的幂
repeat纹理在每个U,V方向上重复多少次,每个方向重复大于1,需要设置wrapS/T 为THREE.RepeatWrapping/THREE.MirroredRepeatWrapping,实现平铺
encoding纹理的着色器编码类型,请参阅texture constants来了解格式的详细信息。

关于纹理贴图的 uv 其实就是坐标,因为xyz已经被顶点坐标占用了,所有用uvw就用来表示纹理坐标。是贴图映射到模型表面的依据,把表面的点和平面的像素对应起来,一般取值在0~1。

u:图片在显示器水平的坐标
v:垂直方向
w:垂直于显示器表面
一般情况只是在表面贴图,就涉及不到w,所以常称为uv。

import brick_diffuse from '../../asset/lights/brick_diffuse.jpg';
import brick_bump from '../../asset/lights/brick_bump.jpg';
import earth_atmos_2048 from '../../asset/lights/earth_atmos_2048.jpg';
import earth_specular_2048 from '../../asset/lights/earth_specular_2048.jpg';

let cubeMat = new THREE.MeshStandardMaterial( {
    roughness: 0.7,
    color: 0xffffff,
    bumpScale: 0.002,
    metalness: 0.2
} );
textureLoader.load( brick_diffuse, function ( map ) {
    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    map.anisotropy = 4;
    map.repeat.set( 1, 1 );
    map.encoding = THREE.sRGBEncoding;
    cubeMat.map = map;
    cubeMat.needsUpdate = true;

} );
textureLoader.load( brick_bump, function ( map ) {
    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    map.anisotropy = 4;
    map.repeat.set( 1, 1 );
    cubeMat.bumpMap = map;
    cubeMat.needsUpdate = true;

} );

let ballMat = new THREE.MeshStandardMaterial( {
    color: 0xffffff,
    roughness: 0.5,
    metalness: 1.0
} );
textureLoader.load( earth_atmos_2048, function ( map ) {

    map.anisotropy = 4;
    map.encoding = THREE.sRGBEncoding;
    ballMat.map = map;
    ballMat.needsUpdate = true;

} );
textureLoader.load( earth_specular_2048, function ( map ) {
    map.anisotropy = 4;
    map.encoding = THREE.sRGBEncoding;
    ballMat.metalnessMap = map;
    ballMat.needsUpdate = true;

} );

加载立方体和地球纹理贴图。

const floorGeometry = new THREE.PlaneGeometry( 20, 20 );
const floorMesh = new THREE.Mesh( floorGeometry, floorMat );
floorMesh.receiveShadow = true;
floorMesh.rotation.x = - Math.PI / 2.0;
scene.add( floorMesh );

const ballGeometry = new THREE.SphereGeometry( 0.25, 32, 32 );
const ballMesh = new THREE.Mesh( ballGeometry, ballMat );
ballMesh.position.set( 1, 0.25, 1 );
ballMesh.rotation.y = Math.PI;
ballMesh.castShadow = true;
scene.add( ballMesh );

const boxGeometry = new THREE.BoxGeometry( 0.5, 0.5, 0.5 );
const boxMesh = new THREE.Mesh( boxGeometry, cubeMat );
boxMesh.position.set( - 0.5, 0.25, - 1 );
boxMesh.castShadow = true;
scene.add( boxMesh );

const boxMesh2 = new THREE.Mesh( boxGeometry, cubeMat );
boxMesh2.position.set( 0, 0.25, - 5 );
boxMesh2.castShadow = true;
scene.add( boxMesh2 );

const boxMesh3 = new THREE.Mesh( boxGeometry, cubeMat );
boxMesh3.position.set( 7, 0.25, 0 );
boxMesh3.castShadow = true;
scene.add( boxMesh3 );

创建地面、球体、立方体的模型,并与材质贴图生成mesh添加到场景中。将 castShadow 属性设置为true。来与光照计算产生阴影效果。

renderer.physicallyCorrectLights = true;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.setPixelRatio( window.devicePixelRatio ); // 设置设备像素比与浏览器一致。
属性描述
physicallyCorrectLights是否使用物理上正确的光照模式。
outputEncoding定义渲染器的输出编码。
shadowMap阴影贴图设置,enabled属性如果设置开启,允许在场景中使用阴影贴图
toneMapping色调映射, 这个属性用于在普通计算机显示器或者移动设备屏幕等低动态范围介质上,模拟、逼近高动态范围(HDR)效果。详情查看Renderer constants
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 1;
controls.maxDistance = 20;

OrbitControls轨道控制器,minDistance 和 maxDistance 表示镜头距离原点的最近和最远距离。

function animate() {
    requestAnimationFrame(animate);
    render();
}
function render() {
    const time = Date.now() * 0.0005;
    bulbLight.position.y = Math.cos( time ) * 0.75 + 1.25;
    renderer.render(scene, camera);
}
animate();

这里render函数中我们获取时间经过Math.cos 后再与常数相加赋值给灯泡的Y 轴位置。

1657968080735.jpg

Math.cos 是数学的余弦函数,是具有周期性的,time的增长会使 Math.cos( time ) 的值在-1~1之间徘徊,灯泡Y轴的数值也就在以1.25高度为中心,上下0.75的距离来回运动,且由于余弦函数的性质,数值变化速度为两头慢中间快,这样看起来灯泡运动也显得很自然。

lights.gif

window.addEventListener( 'resize', onWindowResize );
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}

添加浏览器窗口大小调整时的事件监听,这样确保3D场景充满整个浏览器界面。

添加 Stats 和 GUI

3D渲染是很耗费性能的,只凭我们肉眼对界面卡不卡来观察性能是不太现实的,three.js官方提供了一个辅助工具 Stats 用来帮助我们监测动画运行时的帧数。

import Stats from 'three/examples/jsm/libs/stats.module.js';
stats = new Stats();
document.body.appendChild(stats.dom);
function render() {
    ...
    stats.update();
}

这样就添加stats 到页面左上角,stats 界面有三种,分别表示性能帧数每次刷新所用时间占用内存。可以用鼠标左键点击进行切换,代码中也可以使用setMode() 设置不同的界面显示。

1657970058148.jpg

three.js 官方提供了一个图形控制界面 GUI 组件,可以用来帮助我们实现与3D场景的交互。

import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
const gui = new GUI();

GUI 提供了勾选框、拉动条、下拉框、颜色选择器等等交互方法。

const bulbLuminousPowers : { [key: string]: number } = {
    "110000 lm (1000W)": 110000,
    "3500 lm (300W)": 3500,
    "1700 lm (100W)": 1700,
    "800 lm (60W)": 800,
    "400 lm (40W)": 400,
    "180 lm (25W)": 180,
    "20 lm (4W)": 20,
    "Off": 0
};

const hemiLuminousIrradiances: { [key: string]: number } = {
    "0.0001 lx (Moonless Night)": 0.0001,
    "0.002 lx (Night Airglow)": 0.002,
    "0.5 lx (Full Moon)": 0.5,
    "3.4 lx (City Twilight)": 3.4,
    "50 lx (Living Room)": 50,
    "100 lx (Very Overcast)": 100,
    "350 lx (Office Room)": 350,
    "400 lx (Sunrise/Sunset)": 400,
    "1000 lx (Overcast)": 1000,
    "18000 lx (Daylight)": 18000,
    "50000 lx (Direct Sun)": 50000
};
let previousShadowMap = false;

const params = {
    shadows: true,
    exposure: 0.68,
    bulbPower: Object.keys( bulbLuminousPowers )[ 4 ],
    hemiIrradiance: Object.keys( hemiLuminousIrradiances )[ 0 ]
};
gui.add( params, 'hemiIrradiance', Object.keys( hemiLuminousIrradiances ) );
gui.add( params, 'bulbPower', Object.keys( bulbLuminousPowers ) );
gui.add( params, 'exposure', 0, 1 );
gui.add( params, 'shadows' );
gui.open();
function render() {
    ...
    // 渲染器色调映射的曝光级别
    renderer.toneMappingExposure = Math.pow( params.exposure, 5.0 ); // to allow for very bright scenes.
    // 是否有阴影
    renderer.shadowMap.enabled = params.shadows;
    bulbLight.castShadow = params.shadows;

    if ( params.shadows !== previousShadowMap ) {
        ballMat.needsUpdate = true;
        cubeMat.needsUpdate = true;
        floorMat.needsUpdate = true;
        previousShadowMap = params.shadows;

    }
    // 光照功率
    bulbLight.power = bulbLuminousPowers[ params.bulbPower ];
    // 放射光强度。调节发光颜色。
    bulbMat.emissiveIntensity = bulbLight.intensity / Math.pow( 0.02, 2.0 ); // convert from intensity to irradiance at bulb surface
    // 光照强度
    hemiLight.intensity = hemiLuminousIrradiances[ params.hemiIrradiance ];
}

GUI 提供了hide()show() 的方法来控制 GUI 界面的隐藏展示,也可以通过热键 H 来隐藏显示界面。

如果上面引入GUI 时报错无法引入,可以单独安装GUI :

// 控制台使用 npm 安装
npm install --save dat.gui
// 在代码中引入
import { GUI } from 'dat.gui';

lights2.gif

结语

本篇是根据官网示例 webgl_lights_physical 进行学习解读,示例中展示光线与物体之间的作用,在最终展示结果中灯光照射物体在地板上留下了阴影,并且在随着灯光在上下游走,阴影的大小也在不停的变换,与现实场景很相似,这也是three.js 想要达到的效果。