在Arcgis js官方示例中,有基于BaseLayerViewGL2D写的动态线图层案例。(案例地址)
该案例仅在mapview下有效,sceneview就不显示了,下面将结合threejs对该案例进行修改,实现sceneview下的三维动态线图层。
我们会使用到threejs和externalRenderers等一些其他的arcgis模块,所以先引入他们。
<script type="module">
import * as THREE from './three.js';<!-- 在开头引入three.js文件 -->
require([
...
//在arcgis require中引入这些模块
"esri/views/layers/LayerView",
"esri/views/3d/externalRenderers",
"esri/geometry/geometryEngine",
"esri/geometry/projection",
"esri/geometry/Polyline",
"esri/views/SceneView"
], (
...
LayerView,
externalRenderers,
geometryEngine,
projection,
Polyline,
SceneView
)
</script>
CustomLayerView3D我们会继承layerview,使他得以利用layerview的生命周期去做一些创建销毁。
同时我们会利用externalRenderers,这是sceneview叠加我们自定义的webgl内容的关键。
先将原有案例页面的代码复制到你的编译器中,创建CustomLayerView3D ,我们会需要重写如下这些方法
const CustomLayerView3D = LayerView.createSubclass({
view: null,
layer: null,
constructor({ view, layer }) {
this.view = view;
this.layer = layer;
},
destroy() {},
setup(context) {},
render(context) {},
})
修改CustomLayer.createLayerView引入CustomLayerView3D
...
const CustomLayer = GraphicsLayer.createSubclass({
createLayerView: function (view) {
if (view.type === "2d") {
return new CustomLayerView2D({
view: view,
layer: this
});
} else {
const layerView = new CustomLayerView3D({
view: view,
layer: this
});
externalRenderers.add(view, layerView)
return layerView;
}
}
});
修改mapview为sceneview
const view = new SceneView({
container: "viewDiv",
map: map,
center: [-74.006, 40.7128],
zoom: 15
});
编写setup方法,创建threejs scene,并创建线对象
setup(context) {
this.renderer = new THREE.WebGLRenderer({
context: context.gl, // 可用于将渲染器附加到已有的渲染环境(RenderingContext)中
premultipliedAlpha: false, // renderer是否假设颜色有 premultiplied alpha. 默认为true
});
this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
this.renderer.setViewport(0, 0, this.view.width, this.view.height); // 视口大小设置
this.renderer.autoClearDepth = false; // 定义renderer是否清除深度缓存
this.renderer.autoClearStencil = false; // 定义renderer是否清除模板缓存
this.renderer.autoClearColor = false; // 定义renderer是否清除颜色缓存
this.scene = new THREE.Scene(); // 场景
this.camera = new THREE.PerspectiveCamera(this.view.camera.fov, this.view.canvas.width / this.view.canvas.height); // 相机
this.u_current_time = {
value: 0.0
}
this.updatePositions();
context.resetWebGLState();
},
在updatePositions方法中遍历图层graphics生成three.Mesh对象,我们使用TubeGeometry来做三维线。为了使线条动起来,我们使用自定义的shader材质,传入时间参数u_current_time,并在每次渲染时改变它。
updatePositions() {
this.removeAllLine()
const graphics = this.layer.graphics;
for (let i = 0; i < graphics.items.length; ++i) {
const graphic = graphics.items[i];
const path = graphic.geometry.paths[0];
const distance = this.getLength(new Polyline({
paths: [path],
spatialReference: graphic.geometry.spatialReference
}))
const color = this.getVec4Color(graphic.attributes["color"]);
//在原来的片段着色器上改改重新实现动态效果
const fragmentSource = `
precision highp float;
varying vec2 vUv;
uniform float u_current_time;
uniform vec4 v_color;
uniform float v_distance;
const float TRAIL_SPEED = 50.0;
const float TRAIL_LENGTH = 300.0;
const float TRAIL_CYCLE = 1000.0;
void main(void) {
float speed = TRAIL_SPEED/v_distance;
float len = TRAIL_LENGTH/v_distance;
float cycle = TRAIL_CYCLE/v_distance;
float d = mod(vUv.x - u_current_time * speed, cycle);
float a1 = d < len ? mix(0.0, 1.0, d / len) : 0.0;
float a2 = exp(-abs((vUv.y-0.5)*2.0) * 3.0);
float a = a1 * a2;
gl_FragColor = v_color * a;
}`;
const material = new THREE.ShaderMaterial({
transparent: true,
depthTest: false,
uniforms:
{
u_current_time: this.u_current_time,
v_distance: {
value: distance
},
v_color: {
value: new THREE.Vector4(...color)
},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: fragmentSource
});
const points = this.toRenderCoordinates(path.map(p => [...p, 20])).vector3List
let curve = new THREE.CatmullRomCurve3(points)
//多边形面数越多越吃内存,暂未优化的情况下,加载案例中的所有数据需要2G内存……
const geometry = new THREE.TubeGeometry(curve, points.length * 5, 20, 12, false)
const line = new THREE.Mesh(geometry, material);
line.castShadow = true;
line.receiveShadow = false;
this.scene.add(line);
this.catchLines.push(line);
}
},
removeAllLine() {
this.catchLines.forEach(line => {
this.scene.remove(line)
});
this.catchLines = []
},
getLength(geom) {
let newGeom = geom
if (geom.spatialReference.isGeographic) {
newGeom = projection.project(geom, {
wkid: 3857
})
}
return geometryEngine.planarLength(newGeom, "meters")
},
getVec4Color(color) {
return [
color[0] / 255,
color[1] / 255,
color[2] / 255,
1
]
},
toRenderCoordinates(points) {
if (!points || !Array.isArray(points) || points.length === 0) {
return {
vector3List: [],
points: []
}
}
let vector3List; // 顶点数组
let renderCoordinates = new Array(points.length * 3);
let geographicCoordinates = []
points.forEach(point => {
geographicCoordinates.push(...point)
})
externalRenderers.toRenderCoordinates(this.view, geographicCoordinates, 0,
this.view.spatialReference, renderCoordinates, 0, points.length);
vector3List = points.map((p, i) => {
return new THREE.Vector3(renderCoordinates[i * 3], renderCoordinates[i * 3 + 1], renderCoordinates[i * 3 + 2])
})
return {
vector3List,
points
}
}
编写render方法,同步相机,修改动态参数
render(context) {
if (!this.layer.visible) {
return;
}
// 更新相机参数
const cam = context.camera;
this.camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]);
this.camera.up.set(cam.up[0], cam.up[1], cam.up[2]);
this.camera.lookAt(
new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2])
);
// 投影矩阵可以直接复制
this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
this.renderer.state.reset();
context.bindRenderTarget()
this.u_current_time.value = performance.now() / 1000.0
this.renderer.render(this.scene, this.camera);
externalRenderers.requestRender(this.view);
context.resetWebGLState();
},
constructor中增加projection.load以便后续使用
constructor({ view, layer }) {
this.view =view;
this.layer = layer;
projection.load();
},
destroy编写清除代码
destroy() {
this.handles.destroy()
this.clearScene()//清理threejs
externalRenderers.remove(this.view, this)//从externalRenderers中删除自身
this.view.layerViews.remove(this)//从view.laerviews中删除自身
},
clearScene() {
if (this.scene) {
this.scene.traverse((child) => {
if (child.material) {
child.material.dispose();
}
if (child.geometry) {
child.geometry.dispose();
}
child = null;
});
this.renderer.forceContextLoss();
this.renderer.dispose();
this.scene.clear();
this.catchLines = [];
this.scene = null;
this.camera = null;
this.renderer.domElement = null;
this.renderer = null;
}
},
完成——
示例数据四百多条线,每条几十到几百个坐标点,创建tube后占用内存超过2个G,后续需找方法优化。电脑性能不好的朋友最好不要把数据全加进来,适当减少一些数据量,不然页面可能报内存不足而崩溃。
思想:利用自定义 attribute,复用Material对象。
修改constructor方法,在对象初始化时创建公共Material对象
constructor({ view, layer }) {
this.view = view;
this.layer = layer;
this.material = this.initMaterial()
projection.load();
},
initMaterial() {
this.u_current_time = {
value: 0.0
}
const vertexShader = `
attribute vec4 v_color;
attribute float v_distance;
varying vec2 vUv;
varying vec4 vcolor;
varying float vdistance;
void main() {
vUv = uv;
vcolor = v_color;
vdistance = v_distance;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`
const fragmentSource = `
precision highp float;
varying vec2 vUv;
varying vec4 vcolor;
varying float vdistance;
uniform float u_current_time;
const float TRAIL_SPEED = 50.0;
const float TRAIL_LENGTH = 300.0;
const float TRAIL_CYCLE = 1000.0;
void main(void) {
float speed = TRAIL_SPEED/vdistance;
float len = TRAIL_LENGTH/vdistance;
float cycle = TRAIL_CYCLE/vdistance;
float d = mod(vUv.x - u_current_time * speed, cycle);
float a1 = d < len ? mix(0.0, 1.0, d / len) : 0.0;
float a2 = exp(-abs((vUv.y-0.5)*2.0) * 3.0);
float a = a1 * a2;
gl_FragColor = vcolor * a;
}`;
return new THREE.ShaderMaterial({
transparent: true,
depthTest: false,
uniforms:
{
u_current_time: this.u_current_time,
},
vertexShader: vertexShader,
fragmentShader: fragmentSource
});
},
修改updatePositions,给geometry写入attribute
updatePositions(){
...
const geometry = new THREE.TubeGeometry(curve, points.length * 5, 20, 12, false)
const colors = new Float32Array(geometry.attributes.position.count * 4)
const distances = new Float32Array(geometry.attributes.position.count)
for (let i = 0; i < geometry.attributes.position.count; i++) {
colors[i * 4] = color[0]
colors[i * 4 + 1] = color[1]
colors[i * 4 + 2] = color[2]
colors[i * 4 + 3] = color[3]
distances[i] = distance
}
geometry.setAttribute('v_color', new THREE.Float32BufferAttribute(colors, 4))
geometry.setAttribute('v_distance', new THREE.Float32BufferAttribute(distances, 1))
const line = new THREE.Mesh(geometry, this.material)//改成了使用公共material
...
}
优化后可加载案例所有数据,内存占用不超过100M。