1. demo效果
2. 实现要点
2.1 创建shader材质
这一步中主要是为了实现楼宇从下到上的扫光,而定制的着色器材质,在着色器材质中书写顶点着色器和片元着色器,具体如下
let buildingSweepingLightShader = {
uniforms: {
"boxH": {
type: "f",
value: -10.0
}
},
vertexShader: `
varying vec3 vColor;
varying float v_pz;
void main(){
v_pz = position.y;
vColor = color;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float boxH;
varying vec3 vColor;
varying float v_pz;
float plot(float pct){
return smoothstep(pct-8.0,pct,v_pz) - smoothstep(pct,pct+0.02,v_pz);
}
void main(){
float f1 = plot(boxH);
vec4 b1 = mix(vec4(0.9,0.2,1.0,0.1),vec4(f1,f1,f1,1.0),0.1);
gl_FragColor = mix(vec4(vColor,1.0),b1,f1);
gl_FragColor = vec4(vec3(gl_FragColor),0.9);
}
`
};
const material = new THREE.ShaderMaterial({
uniforms: buildingSweepingLightShader.uniforms,
vertexShader: buildingSweepingLightShader.vertexShader,
fragmentShader: buildingSweepingLightShader.fragmentShader,
vertexColors: buildingSweepingLightShader
})
material.needsUpdate = true
2.2 创建模拟楼宇Mesh
这里主要使用上一步创建的着色器材质和使用BoxBufferGeometry创建的几何体创建Mesh对象,并随机生成高度模拟楼宇
function initModel() {
//创建60个立方体模拟楼宇
for (let i = 0; i < 60; i++) {
const height = Math.random() * 10 + 2
const width = 3
const cubeGeom = new THREE.BoxBufferGeometry(width, height, width)
cubeGeom.setAttribute('color', new THREE.BufferAttribute(new Float32Array(24 * 3), 3))
const colors = cubeGeom.attributes.color
let r = Math.random() * 0.2,
g = Math.random() * 0.1,
b = Math.random() * 0.8
//设置立方体六个面24个顶点的颜色
for (let i = 0; i < 24; i++) {
colors.setXYZ(i, r, g, 0.6)
}
//重置立方体顶部四边形的四个顶点的颜色
const k = 2
colors.setXYZ(k * 4 + 0, .0, g, 1.0)
colors.setXYZ(k * 4 + 1, .0, g, 1.0)
colors.setXYZ(k * 4 + 2, .0, g, 1.0)
colors.setXYZ(k * 4 + 3, .0, g, 1.0)
const cube = new THREE.Mesh(cubeGeom, material)
cube.position.set(Math.random() * 100 - 50, height / 2, Math.random() * 100 - 50)
scene.add(cube)
//绘制边框线
const lineGeom = new THREE.EdgesGeometry(cubeGeom)
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x018BF5,
linewidth: 1,
linecap: 'round',
linejoin: 'round'
})
const line = new THREE.LineSegments(lineGeom, lineMaterial)
line.scale.copy(cube.scale)
line.rotation.copy(cube.rotation)
line.position.copy(cube.position)
scene.add(line)
}
}
2.3 render中更新uniform变量
这一步主要是通过着色器材质中的uniform变量boxH向着色器中传值楼宇中扫光的高度,具体如下
buildingSweepingLightShader.uniforms.boxH.value += 0.1
if (buildingSweepingLightShader.uniforms.boxH.value > 10) {
buildingSweepingLightShader.uniforms.boxH.value = -10
}
3. demo代码
<!DOCTYPE html>
<html>
<head>
<title>Example 12 - buildingSweepingLight</title>
<script type="text/javascript" src="../three/build/three.js"></script>
<script type="text/javascript" src="../three/examples/js/controls/OrbitControls.js"></script>
<script type="text/javascript" src="../three/examples/js/libs/stats.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output"></div>
<div id="WebGL-output"></div>
<script type="text/javascript">
let stats, controls;
let camera, scene, renderer;
let buildingSweepingLightShader = {
uniforms: {
"boxH": {
type: "f",
value: -10.0
}
},
vertexShader: `
varying vec3 vColor;
varying float v_pz;
void main(){
v_pz = position.y;
vColor = color;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float boxH;
varying vec3 vColor;
varying float v_pz;
float plot(float pct){
return smoothstep(pct-8.0,pct,v_pz) - smoothstep(pct,pct+0.02,v_pz);
}
void main(){
float f1 = plot(boxH);
vec4 b1 = mix(vec4(0.9,0.2,1.0,0.1),vec4(f1,f1,f1,1.0),0.1);
gl_FragColor = mix(vec4(vColor,1.0),b1,f1);
gl_FragColor = vec4(vec3(gl_FragColor),0.9);
}
`
};
const material = new THREE.ShaderMaterial({
uniforms: buildingSweepingLightShader.uniforms,
vertexShader: buildingSweepingLightShader.vertexShader,
fragmentShader: buildingSweepingLightShader.fragmentShader,
vertexColors: buildingSweepingLightShader
})
material.needsUpdate = true
function initScene() {
scene = new THREE.Scene();
}
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(30, 40, 80)
camera.lookAt(new THREE.Vector3(0, 0, 0));
}
function initLight() {
//添加环境光
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(5, 10, 20);
spotLight.castShadow = true;
scene.add(spotLight);
}
function initModel() {
//创建60个立方体模拟楼宇
for (let i = 0; i < 60; i++) {
const height = Math.random() * 10 + 2
const width = 3
const cubeGeom = new THREE.BoxBufferGeometry(width, height, width)
cubeGeom.setAttribute('color', new THREE.BufferAttribute(new Float32Array(24 * 3), 3))
const colors = cubeGeom.attributes.color
let r = Math.random() * 0.2,
g = Math.random() * 0.1,
b = Math.random() * 0.8
//设置立方体六个面24个顶点的颜色
for (let i = 0; i < 24; i++) {
colors.setXYZ(i, r, g, 0.6)
}
//重置立方体顶部四边形的四个顶点的颜色
const k = 2
colors.setXYZ(k * 4 + 0, .0, g, 1.0)
colors.setXYZ(k * 4 + 1, .0, g, 1.0)
colors.setXYZ(k * 4 + 2, .0, g, 1.0)
colors.setXYZ(k * 4 + 3, .0, g, 1.0)
const cube = new THREE.Mesh(cubeGeom, material)
cube.position.set(Math.random() * 100 - 50, height / 2, Math.random() * 100 - 50)
scene.add(cube)
//绘制边框线
const lineGeom = new THREE.EdgesGeometry(cubeGeom)
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x018BF5,
linewidth: 1,
linecap: 'round',
linejoin: 'round'
})
const line = new THREE.LineSegments(lineGeom, lineMaterial)
line.scale.copy(cube.scale)
line.rotation.copy(cube.rotation)
line.position.copy(cube.position)
scene.add(line)
}
}
function initRender() {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
})
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x0f2d48, 1) // 设置背景颜色
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.getElementById("WebGL-output").appendChild(renderer.domElement);
}
//初始化轨道控制器
function initControls() {
clock = new THREE.Clock() // 创建THREE.Clock对象,用于计算上次调用经过的时间
controls = new THREE.OrbitControls(camera, renderer.domElement)
}
function init() {
initScene();
initCamera();
initLight();
initRender();
initStats();
initControls();
initModel();
render();
}
function updateFun() {
stats.update();
const delta = clock.getDelta() // 获取自上次调用的时间差
controls.update(delta) // 相机更新
buildingSweepingLightShader.uniforms.boxH.value += 0.1
if (buildingSweepingLightShader.uniforms.boxH.value > 10) {
buildingSweepingLightShader.uniforms.boxH.value = -10
}
}
function render() {
updateFun()
requestAnimationFrame(render);
renderer.render(scene, camera);
}
function initStats() {
stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
document.getElementById("Stats-output").appendChild(stats.domElement);
}
window.onload = init;
</script>
</body>
</html>