1. 需要掌握的知识
- webGL shader编程
- three.js基础用法
2. antv L7城市扫光效果
上图我们可以看到一个个的光波从城市中心像远处扩散,光波略过的高楼与地面会呈现高亮的状态,并且根据光波的宽度(大圈的半径 - 小圈的半径)高亮还呈现出渐变的状态。
在WebGL编程指南-27 逐片元处理点光源光照效果我们知道,点光源照射在物体上的明暗效果,是通过该片元到顶点距离跟顶点法线来决定的,那我们可以借此思路来实现,大致思路是一个不断变大的圆,这个圆分大圆与小圆,在大圆与小圆之间的片元呈高光效果
3. 代码实现
我们借助three.js官网提供的一个例子画出城市,在此例子基础上实现扫光效果
3.1 three.js demo效果
1. 首先给城市添加地面
const planeGeo = new THREE.PlaneBufferGeometry(1700, 1700);
ground_shadermaterial = new THREE.ShaderMaterial({
uniforms: {
//小圆圈半径
innerCircleWidth: {
value: 0,
},
//大圆圈半径=innerCircleWidth + circleWidth
circleWidth: {
value: shaderCircleWidth,
},
diff: {
value: new THREE.Color(0.2, 0.2, 0.2),
},
color: {
value: new THREE.Color(0.0, 0.0, 1.0),
},
opacity: {
value: 0.3,
},
//圆心位置
center: {
value: new THREE.Vector3(0, 0, 0),
},
},
vertexShader: ground_vertexShader,
fragmentShader: ground_fragmentShader,
// side: THREE.DoubleSide, // 双面可见
transparent: true,
});
const ground = new THREE.Mesh(planeGeo, ground_shadermaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
2. 给大楼添加纹理贴图
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.translate(0, 0.5, 0);
//添加纹理
// const material = new THREE.MeshPhongMaterial({
// color: 0xffffff,
// flatShading: true,
// map: new THREE.TextureLoader().load("./textures/building.png"),
// });
//顶点着色器
const vertexShader = `
varying vec2 vUv;
varying vec3 v_position;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
v_position = vec3(modelMatrix * vec4(position, 1.0));
}
`;
//片元着色器
const fragmentShader = `
varying vec2 vUv;
varying vec3 v_position;
uniform float innerCircleWidth;
uniform float circleWidth;
uniform float opacity;
uniform vec3 center;
uniform vec3 color;
uniform sampler2D buliding;
void main() {
float dis = length(v_position - center);
if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
float r = (dis - innerCircleWidth) / circleWidth;
vec4 tex = texture2D( buliding, vUv);
gl_FragColor = mix(tex, vec4(color, opacity), r);
}else {
gl_FragColor = texture2D( buliding, vUv);
}
}
`;
const map = new THREE.TextureLoader().load("./textures/building.png");
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
material = new THREE.ShaderMaterial({
uniforms: {
buliding: {
value: map,
},
innerCircleWidth: {
value: 0,
},
circleWidth: {
value: shaderCircleWidth,
},
color: {
value: new THREE.Color(0.0, 0.0, 1.0),
},
opacity: {
value: 0.9,
},
center: {
value: new THREE.Vector3(0, 0, 0),
},
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
// side: THREE.DoubleSide, // 双面可见
transparent: true,
});
3. 修改相机可视范围
由于demo的相机可视范围太小,为了方便查看效果更改一下相机范围
controls.minDistance = 100;
controls.maxDistance = 5000;
4.核心逻辑
我们在 ground_fragmentShader(地面的着色器)中发现此片元的颜色,取决于该片元到地图中心(center)的距离是否在光波范围(circleWidth属性),下面用到了mix函数这是一个插值函数,为了让我们的高亮与片元在光波中的位置有渐变效果,我们用此方法
float dis = length(v_position - center);
if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
float r = (dis - innerCircleWidth) / circleWidth;
gl_FragColor = mix(vec4(diff, 0.1), vec4(color, opacity), r);
}else {
gl_FragColor = vec4(diff, 0.1);
fragmentShader(高楼的着色器)逻辑类似取当前片元的纹素通过mix函数求出一个“高亮”颜色
if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
float r = (dis - innerCircleWidth) / circleWidth;
vec4 tex = texture2D( buliding, vUv);
gl_FragColor = mix(tex, vec4(color, opacity), r);
}else {
gl_FragColor = texture2D( buliding, vUv);
}
最后在animate函数中改变innerCircleWidth的大小,使其不断变大(也就是小圆的半径不断变大)
function animate() {
requestAnimationFrame(animate);
controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
//这里的20就是圆半径的扩大速度
material.uniforms.innerCircleWidth.value += 20;
if (material.uniforms.innerCircleWidth.value > 1700) {
material.uniforms.innerCircleWidth.value = -shaderCircleWidth;
}
ground_shadermaterial.uniforms.innerCircleWidth.value += 20;
if (ground_shadermaterial.uniforms.innerCircleWidth.value > 1700) {
ground_shadermaterial.uniforms.innerCircleWidth.value =
-shaderCircleWidth;
}
render();
}
5.demo效果
6.demo代码
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - postprocessing - unreal bloom</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
color: #fff;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
</style>
<script src="./three.js"></script>
<script src="./OrbitControls.js"></script>
<script src="./BufferGeometryUtils.js"></script>
</head>
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
varying vec3 v_position;
void main() {
vUv = uv;
v_position = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-vertex" id="fragmentShader">
varying vec2 vUv;
varying vec3 v_position;
uniform float innerCircleWidth;
uniform float circleWidth;
uniform float opacity;
uniform vec3 center;
uniform vec3 color;
uniform sampler2D texture;
void main() {
float dis = length(v_position - center);
if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
float r = (dis - innerCircleWidth) / circleWidth;
vec4 tex = texture2D( texture, vUv);
gl_FragColor = mix(tex, vec4(color, opacity), r);
}else {
gl_FragColor = texture2D( texture, vUv);
}
}
</script>
<script type="x-shader/x-vertex" id="ground_vertexShader">
varying vec2 vUv;
varying vec3 v_position;
void main() {
vUv = uv;
v_position = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-vertex" id="ground_fragmentShader">
varying vec2 vUv;
varying vec3 v_position;
uniform float innerCircleWidth;
uniform float circleWidth;
uniform float opacity;
uniform vec3 center;
uniform vec3 color;
uniform vec3 diff;
void main() {
float dis = length(v_position - center);
if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
float r = (dis - innerCircleWidth) / circleWidth;
gl_FragColor = mix(vec4(diff, 0.1), vec4(color, opacity), r);
}else {
gl_FragColor = vec4(diff, 0.1);
}
}
</script>
<body>
<div id="container"></div>
<script>
console.log(1298371298739123)
var scene, camera, controls, pointLight
var renderer, shadermaterial, ground_shadermaterial, shaderCircleWidth = 300
var clock = new THREE.Clock()
var container = document.getElementById( 'container' )
// 数据池,为防止轮播时请求数据出现延迟,以及点击时快速进入下一级地图
var mapDataPool = {}
var height = 30
var MAP_SIZE = 1000, MAP_WIDTH = MAP_SIZE, MAP_HEIGHT = MAP_SIZE
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } )
renderer.setPixelRatio( window.devicePixelRatio )
renderer.setClearColor('#000')
renderer.setSize( window.innerWidth, window.innerHeight )
renderer.toneMapping = THREE.ReinhardToneMapping
container.appendChild( renderer.domElement )
scene = new THREE.Scene()
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 10000 )
camera.position.set( 200, 200, 200 )
scene.add( camera )
controls = new THREE.OrbitControls( camera, renderer.domElement )
/**
* 光源设置
*/
// 方向光1
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.9)
directionalLight.position.set(400, 200, 300)
directionalLight.layers.enable(1)
scene.add(directionalLight)
scene.add( new THREE.AmbientLight( 0x404040 ) )
pointLight = new THREE.PointLight( 0xffffff, 1 )
camera.add( pointLight )
var minLng = null, minLat = null, maxLng = null, maxLat = null
var mainMapGroup = new THREE.Group()
scene.add(mainMapGroup)
let map = new THREE.TextureLoader().load('./building.png')
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
var texMAT = new THREE.MeshBasicMaterial({ map })
var mat = new THREE.MeshPhongMaterial({
transparent: true,
// opacity: 1,
// color: new THREE.Color(1, 1, 0),
side: THREE.BackSide,
map: new THREE.TextureLoader().load('./building_top.png')
})
var mat2 = new THREE.MeshBasicMaterial({
transparent: true,
// opacity: 1,
// color: new THREE.Color(1,0,0),
map: new THREE.TextureLoader().load('./building_top.png')
})
shadermaterial = new THREE.ShaderMaterial( {
uniforms: {
texture: {
value: map
},
innerCircleWidth: {
value: 0
},
circleWidth: {
value: shaderCircleWidth
},
color: {
value: new THREE.Color(0.0, 0.0, 1.0)
},
opacity: {
value: 0.9
},
center: {
value: new THREE.Vector3(0, 0, 0)
}
},
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
// side: THREE.DoubleSide, // 双面可见
transparent: true
} );
ground_shadermaterial = new THREE.ShaderMaterial( {
uniforms: {
innerCircleWidth: {
value: 0
},
circleWidth: {
value: shaderCircleWidth
},
diff: {
value: new THREE.Color(0.2, 0.2, 0.2)
},
color: {
value: new THREE.Color(0.0, 0.0, 1.0)
},
opacity: {
value: 0.3
},
center: {
value: new THREE.Vector3(0, 0, 0)
}
},
vertexShader: document.getElementById( 'ground_vertexShader' ).textContent,
fragmentShader: document.getElementById( 'ground_fragmentShader' ).textContent,
// side: THREE.DoubleSide, // 双面可见
transparent: true
} );
var groundGeo = new THREE.PlaneBufferGeometry( 1000, 1000 );
// var groundMat = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
var ground = new THREE.Mesh( groundGeo, ground_shadermaterial );
ground.rotation.x = -Math.PI/2
scene.add( ground );
fetch('build1.geojson')
.then((response) => {
return response.json();
})
.then((mapJson) => {
var coor = getGeoExtent(mapJson.features)
minLng = coor.minLng,
minLat = coor.minLat,
maxLng = coor.maxLng,
maxLat = coor.maxLat
let geos = []
let sideGeos = [];
let topGeos = [];
let topGeos2 = [];
for(let i = 0;i< mapJson.features.length;i++) {
let feature = mapJson.features[i]
if (!feature.geometry) return;
const coordinates = feature.geometry.coordinates;
switch (feature.geometry.type) {
case 'Polygon':
for (let points of coordinates) {
const _points = points.map((point) => {
return lnglat2Map(point)
})
let floor = feature.properties.Floor
let geometry = createBuildingGeometry(_points, floor)
sideGeos.push(geometry)
let topGeo = createTopGeo(_points, floor)
topGeos.push(topGeo[0])
topGeos2.push(topGeo[1])
}
break;
case 'MultiPolygon':
for(let points of coordinates[0]) {
const _points = points.map((point) => {
return lnglat2Map(point)
})
let floor = feature.properties.Floor
let geometry = createBuildingGeometry(_points, floor)
sideGeos.push(geometry)
let topGeo = createTopGeo(_points, floor)
topGeos.push(topGeo[0])
topGeos2.push(topGeo[1])
}
break;
default:
break;
}
}
let sideGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(sideGeos, false)
// let city1 = new THREE.Mesh(sideGeo, texMAT)
let city1 = new THREE.Mesh(sideGeo, shadermaterial)
city1.rotation.x = -Math.PI/2
scene.add(city1)
let topGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(topGeos, false)
topGeo = new THREE.Geometry().fromBufferGeometry(topGeo);
topGeo.computeFaceNormals()
let city2= new THREE.Mesh(topGeo, mat)
city2.rotation.x = -Math.PI/2
scene.add(city2)
let topGeo2 = THREE.BufferGeometryUtils.mergeBufferGeometries(topGeos2, false)
let city3= new THREE.Mesh(topGeo2, mat2)
city3.rotation.x = -Math.PI/2
scene.add(city3)
})
animate()
window.onresize = function () {
var width = window.innerWidth;
var height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize( width, height );
};
function animate() {
requestAnimationFrame( animate )
renderer.render(scene, camera)
shadermaterial.uniforms.innerCircleWidth.value += 10
if(shadermaterial.uniforms.innerCircleWidth.value > 1000){
shadermaterial.uniforms.innerCircleWidth.value = -shaderCircleWidth
}
ground_shadermaterial.uniforms.innerCircleWidth.value += 10
if(ground_shadermaterial.uniforms.innerCircleWidth.value > 1000){
ground_shadermaterial.uniforms.innerCircleWidth.value = -shaderCircleWidth
}
}
function biseP(p, a, b, d) {
let dStartAngle = Math.atan2(a.y - p.y, a.x - p.x),
dEndAngle = Math.atan2(b.y - p.y, b.x - p.x);
let dWAngle = dEndAngle - dStartAngle; // 外角角度
if (dWAngle < 0) {
dWAngle += 2 * Math.PI;
} else if (dWAngle > 2 * Math.PI) {
dWAngle -= 2 * Math.PI;
} // 这里算出来角度都是弧度单位的
let θ = dWAngle / 2 + dStartAngle; // 计算角平分线上偏移距离
// let l = d;
// 计算垂直偏移距离
let l = d / Math.sin(dWAngle / 2);
if(Math.abs(l) > 2*d) l = d; // 限制异常数据
let panX = l * Math.cos(θ) + p.x,
panY = l * Math.sin(θ) + p.y; // 夹角(内角)平分线的点
return {
x: 2 * p.x - panX,
y: 2 * p.y - panY
};
};
function getOffsetPoint(points, index, offset) {
let len = points.length;
let prePoint, nextPoint;
if (index == 0) {
// 第一个点
prePoint = points[len - 2];
nextPoint = points[index + 1];
} else if (index == len - 1) {
// 最后一个点
prePoint = points[index - 1];
nextPoint = points[1];
} else {
prePoint = points[index - 1];
nextPoint = points[index + 1];
}
return biseP(points[index], prePoint, nextPoint, offset);
};
/**
* @name 生成顶部几何体
* @param {array} points 点集
* @param {number} offset_s 内凹边距
* @param {number} height 总体高度
* @param {number} offset_h 沉降深度
* @return {array} geometries 构造沉降+底部shape
*/
function createTopGeo(points, floor) {
let vertices = []; // 顶点数组
let indices = []; // 索引数组
if(THREE.ShapeUtils.isClockWise( points )) {
points = points.reverse();
}
let offset_h = 0.2, offset_s = 0.1, height = floor
let len = points.length;
let point_offests = points.map((point, index) => {
const point_offest = getOffsetPoint(points, index, offset_s);
const p1 = new THREE.Vector3(point.x, point.y, height);
const p2 = new THREE.Vector3(point_offest.x, point_offest.y, height);
const p3 = new THREE.Vector3(p2.x, p2.y, p2.z - offset_h);
vertices.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z);
if (index != len - 1) {
indices.push(index * 3, index * 3 + 1, (index + 1) * 3 + 1, // 0, 1, 4
(index + 1) * 3 + 1, (index + 1) * 3, index * 3, // 4, 3, 0
index * 3 + 1, index * 3 + 2, (index + 1) * 3 + 2, // 1, 2, 5
(index + 1) * 3 + 2, (index + 1) * 3 + 1, index * 3 + 1 // 5, 4, 1
);
} else {
// 最后一组
indices.push(index * 3, index * 3 + 1, 1, 1, 0, index * 3, index * 3 + 1, index * 3 + 2, 2, 2, 1, index * 3 + 1);
}
return point_offest;
});
let geometry = new THREE.BufferGeometry();
geometry.setIndex(indices);
geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const geo = new THREE.ShapeBufferGeometry(new THREE.Shape(point_offests));
const translateMatrix = new THREE.Matrix4();
translateMatrix.makeTranslation(0, 0, height - offset_h);
geo.applyMatrix(translateMatrix);
return [geometry, geo];
}
function createBuildingGeometry(points, floor) {
let mapInnerSize = floor ; // 贴图对应真实尺寸大小(3m)
let vecHeight = floor;
let vertices = []; // 墙壁顶点数组
let indices = []; // 墙壁索引数组 indices index负数
let uv = []; // 墙壁纹理坐标
// const uv_y = vecHeight / mapInnerSize;
const uv_y = floor
let uv_x = 0;
if(THREE.ShapeUtils.isClockWise( points )) {
points = points.reverse();
}
const len = points.length;
points.forEach((point, index) => {
vertices.push(point.x, point.y, 0); // 底部点
vertices.push(point.x, point.y, vecHeight); // 顶部点
if (index !== 0) {
const prePoint = points[index - 1];
const dis = new THREE.Vector2(prePoint.x, prePoint.y).distanceTo(new THREE.Vector2(point.x, point.y));
uv_x += dis*2 / mapInnerSize;
// uv_x += 1
}
uv.push(uv_x, 0, uv_x, uv_y);
if (index !== 0) {
// 推入面索引
indices.push(index * 2 - 2, index * 2, index * 2 - 1, index * 2 - 1, index * 2, index * 2 + 1);
}
});
// 闭合
const firstPoint = points[0], lastPoint = points[len - 1];
vertices.push(firstPoint.x, firstPoint.y, 0); // 底部点
vertices.push(firstPoint.x, firstPoint.y, vecHeight); // 顶部点
const dis = new THREE.Vector2(firstPoint.x, firstPoint.y).distanceTo(new THREE.Vector2(lastPoint.x, lastPoint.y));
uv_x += dis / mapInnerSize;
uv.push(uv_x, 0, uv_x, uv_y);
indices.push(len * 2 - 2, len * 2, len * 2 - 1, len * 2 - 1, len * 2, len * 2 + 1);
// 构建侧面
let geometry = new THREE.BufferGeometry();
geometry.isBufferGeometry = true
geometry.setIndex(indices);
geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.addAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
return geometry;
}
function getGeoExtent(features) { // 计算数据的最大最小经纬度、最大最小墨卡托坐标以及墨卡托坐标的的多变形数组
let minLng = 180, maxLng = -180, minLat = 90, maxLat = -90
for (let feature of features) {
if (feature.geometry) {
if (feature.geometry.type === 'Polygon') {
for (let points of feature.geometry.coordinates) {
for (let point of points) {
minLng = minLng < point[0] ? minLng : point[0];
maxLng = maxLng > point[0] ? maxLng : point[0];
minLat = minLat < point[1] ? minLat : point[1];
maxLat = maxLat > point[1] ? maxLat : point[1];
}
}
} else if (feature.geometry.type === 'MultiPolygon') {
for (let polygonPoints of feature.geometry.coordinates) {
for (let points of polygonPoints) {
for (let point of points) {
minLng = minLng < point[0] ? minLng : point[0];
maxLng = maxLng > point[0] ? maxLng : point[0];
minLat = minLat < point[1] ? minLat : point[1];
maxLat = maxLat > point[1] ? maxLat : point[1];
}
}
}
}
}
}
return { minLng, minLat, maxLng, maxLat }
}
function lnglat2Map(lnglat) {
let v = new THREE.Vector2(
((lnglat[0] - minLng) / (maxLng - minLng)) * MAP_WIDTH - MAP_WIDTH * 0.5,
((lnglat[1] - minLat) / (maxLat - minLat)) * MAP_HEIGHT - MAP_HEIGHT * 0.5
)
return v
}
</script>
</body>
</html>