1.“冰山一角”
在使用echarts-gl地球的时候,会发现球体边缘是不规则的,例子见 Echarts GL Earth。代码很简单,只使用了贴图,并没有导入模型,“冰山一角”的凹凸如何形成引起了我的兴趣,于是开始了顺藤摸瓜之旅。
2.echarts-gl觅芳踪
安装好依赖之后,开始了debug之路。
- 注释掉
heightTexture
后,冰山一角凹凸性消失,形成了完美球,见下图。
于是可以推断该效果由heightTexture
配置项影响。
- 在echarts-gl中搜索heightTexture
发现了函数getDisplacementTexture
,使用该函数的地方有四处,通过debug,最关键一处在这里(下图)。
ecModel.eachComponent('globe', function (globeModel, idx) {
var globe = globeModel.coordinateSystem;
// Update displacement data
var displacementTextureValue = globeModel.getDisplacementTexture(); // 获取置换纹理
var displacementScale = globeModel.getDisplacemenScale(); // 获取置换比例
if (globeModel.isDisplacementChanged()) {
if (globeModel.hasDisplacement()) {
var immediateLoaded = true;
__WEBPACK_IMPORTED_MODULE_5__util_graphicGL__["a" /* default */].loadTexture(displacementTextureValue, api, function (texture) {
var img = texture.image;
var displacementData = getDisplacementData(img, displacementScale); //获取置换数据
globeModel.setDisplacementData(displacementData.data, displacementData.width, displacementData.height);
if (!immediateLoaded) {
// Update layouts
api.dispatchAction({
type: 'globeUpdateDisplacment'
});
}
});
immediateLoaded = false;
}
else {
globe.setDisplacementData(null, 0, 0);
}
globe.setDisplacementData(
globeModel.displacementData, globeModel.displacementWidth, globeModel.displacementHeight
);
}
});
这里其实核心在于设置displacementData
,于是重点在如何生成displacementData
与如何使用displacementData
。
3.生成displacementData
,从上面的代码块可知其通过getDisplacementData
函数生成,函数定义如下:
function getDisplacementData(img, displacementScale) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var width = img.width;
var height = img.height;
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
var rgbaArr = ctx.getImageData(0, 0, width, height).data;
var displacementArr = new Float32Array(rgbaArr.length / 4);
for (var i = 0; i < rgbaArr.length / 4; i++) {
var x = rgbaArr[i * 4]; // 取红色分量
displacementArr[i] = x / 255 * displacementScale;
}
return {
data: displacementArr,
width: width,
height: height
};
}
该函数将图片(也就是heightTexture提供的图片)画在canvas画布上,然后拿到ImageData,ImageData是一个一维数组,包含图片的像素信息,如下图。
由于每一个像素占四个位置(rgba四个分量),所以displacementArr
的长度是rgbaArr.length / 4
,然后取了红色(red, i * 4)分量, 其实取蓝色分量(i * 4 + 1)、绿色分量(i * 4 + 2)都可以,或者取三者的平均值也可以,效果之后些许不同,与置换纹理图片相关。最后得到的data如下图:
- 使用
displacementData
。通过跟踪,摸到了_doDisplaceVertices
这里,顾名思义,这个函数要做的是处理置换顶点,相当于将原geometry
的顶点信息修改掉(怪不得会有凹凸感,原来是几何体的形状改变了。webgl的核心就是顶点着色器与片元着色器,这一部分知识可以看这里)。
_doDisplaceVertices: function (geometry, globe) {
// 置换顶点
var positionArr = geometry.attributes.position.value; // 顶点的位置信息
var uvArr = geometry.attributes.texcoord0.value; // uv坐标信息
var originalPositionArr = geometry.__originalPosition; // 原始顶点位置信息
if (!originalPositionArr || originalPositionArr.length !== positionArr.length) {
originalPositionArr = new Float32Array(positionArr.length);
originalPositionArr.set(positionArr);
geometry.__originalPosition = originalPositionArr;
}
var width = globe.displacementWidth;
var height = globe.displacementHeight;
var data = globe.displacementData; // 我们的置换数据
// 遍历顶点信息,更新每个位置坐标
for (var i = 0; i < geometry.vertexCount; i++) {
var i3 = i * 3; // 为什么乘以3, 因为每个顶点占3位
var i2 = i * 2; // 为什么乘以2, 因为每个uv坐标占2位
// 原始位置
var x = originalPositionArr[i3 + 1];
var y = originalPositionArr[i3 + 2];
var z = originalPositionArr[i3 + 3];
// 根据uv坐标在placementData中取scale值
var u = uvArr[i2++];
var v = uvArr[i2++];
var j = Math.round(u * (width - 1));
var k = Math.round(v * (height - 1));
var idx = k * width + j;
var scale = data ? data[idx] : 0; // 每个位置的偏移量比例
// 更新顶点信息,每一个点在原始点位置增加一个偏移量,这个偏移量由heightTexture图片某个像素的red值决定。
positionArr[i3 + 1] = x + x * scale;
positionArr[i3 + 2] = y + y * scale;
positionArr[i3 + 3] = z + z * scale;
}
geometry.generateVertexNormals(); // 生成顶点法向量
// 数据标记与更新包围盒
geometry.dirty();
geometry.updateBoundingBox();
},
uv贴图可见Three.js电子书第八章
到这里,基本的逻辑跟踪就结束了。
- 原始的uv坐标信息在生成
SphereGeometry
的时候就已经确定放在attributes中了(根据widthSegments
与widthSegments
生成),echarts-gl依赖的底层库是claygl,感兴趣可以查看其构造函数。
3.three.js入场
于是我想着用three.js
来实现类似的效果。
three.js
来构建三维就像拍一部电影,场景
、演员组
、灯光组
、摄像组
、导演组
缺一不可,只需要按照顺序执行就好,简单来讲可以按下图顺序按部就班。
// atlast
window.onload = async function () {
initScene(); // 场景准备就绪
await initMesh(); // 演员准备就绪
initLight(); // 灯光组准备就绪
initCamera(); // 摄影组准备就绪
initRenderer(); // 导演剪辑渲染镜头给观众
animate(); // 开启动画Action 大家开始动
};
贴图素材(先给需要的玩家,或者去echarts gallery抓包,掘金有水印)
普通贴图完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
<script src="three.js"></script>
<script src="OrbitControls.js"></script>
</head>
<body>
<div id="chart" style="height: 100%;"></div>
<script>
let container = document.getElementById("chart");
let width = container.clientWidth;
let height = container.clientHeight;
let SCENE, CAMERA, RENDERER;
const ImageLoader = new THREE.ImageLoader();
function initScene() {
SCENE = new THREE.Scene();
}
async function initMesh() {
let axisHelper = new THREE.AxesHelper(250);
SCENE.add(axisHelper);
let geometry = new THREE.SphereGeometry(5, 40, 40); // 3,2
let img = await ImageLoader.load("./earth.jpg");
let texture = new THREE.Texture(img);
texture.needsUpdate = true;
let material = new THREE.MeshStandardMaterial({
map: texture,
});
let sphere = new THREE.Mesh(geometry, material);
SCENE.add(sphere);
}
function initLight() {
//点光源
let point = new THREE.PointLight(0xffffff);
point.position.set(400, 0, 0); //点光源位置
SCENE.add(point); //点光源添加到场景中
//环境光
let ambient = new THREE.AmbientLight(0xffffff);
SCENE.add(ambient);
}
function initCamera() {
let k = width / height; //窗口宽高比
let s = 10; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机 CAMERA
CAMERA = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
CAMERA.position.set(200, 100, 100); //设置相机位置
CAMERA.lookAt(SCENE.position); //设置相机方向(指向的场景对象)
}
function initRenderer() {
// RENDERER
RENDERER = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
RENDERER.setSize(width, height); //设置渲染区域尺寸
RENDERER.setClearColor(0x00000, 1); //设置背景颜色
container.appendChild(RENDERER.domElement); //body元素中插入canvas对象
new THREE.OrbitControls(CAMERA, RENDERER.domElement);
}
function animate() {
RENDERER.render(SCENE, CAMERA); //执行渲染操作
requestAnimationFrame(animate);
}
window.onload = async function () {
initScene(); // 场景准备就绪
await initMesh(); // 演员准备就绪
initLight(); // 灯光组准备就绪
initCamera(); // 摄影组准备就绪
initRenderer(); // 导演剪辑渲染镜头给观众
animate(); // 开启动画Action 大家开始动
};
</script>
</body>
</html>
这样可以得到一个完美球
通过群友提示,three.js
的MeshStandardMaterial
材质有displacementMap
(置换贴图配置),这里只需要修改initMesh
async function initMesh() {
let axisHelper = new THREE.AxesHelper(250);
SCENE.add(axisHelper);
let geometry = new THREE.SphereGeometry(5, 40, 40); // 3,2
let img = await ImageLoader.load("./earth.jpg");
let heightImg = await ImageLoader.load("./earth-high.jpg");
let texture = new THREE.Texture(img);
let heightTexture = new THREE.Texture(heightImg);
texture.needsUpdate = true;
let material = new THREE.MeshStandardMaterial({
displacementMap: heightTexture,
displacementScale: 1.1,
displacementBias: 4,
map: texture,
});
material.displacementMap.needsUpdate = true; // very important
let sphere = new THREE.Mesh(geometry, material);
SCENE.add(sphere);
}
注意需要添加material.displacementMap.needsUpdate = true
,不然置换贴图将不生效。
置换贴图(displacementMap)
这要不仅可以看到“冰山一角”,也可以看到“世界屋脊”。
于是我又尝试了法线贴图
与凹凸贴图
法线贴图(normalMap)
凹凸贴图(bumpMap)
4.置换贴图 vs 凹凸贴图 vs 法线贴图
-
置换贴图:改变了Geometry的顶点位置,会产生大量的三角面,计算量极大,吃计算机配置(显卡、内存、CPU),效果最好,因为其实真正的模拟,凸出与凹入都会与光照阴影结合。
-
凹凸贴图:凹凸贴图光照和材质在3D模型的表面制造出凹凸不平的质感的错觉。使用灰度(Grayscale)图和简单的光影技巧在对象的表面人为地制造这种质感,而不是真的在其表面扣出一个个的凸起和凹入。当灰度值在50%附近时,物体表面几乎不会有什么细节变化。当灰度值变亮(白),表面细节呈现为凸出,当灰度值变暗(黑),表面细节呈现为凹入。用凹凸贴图实现模型的微小细节非常棒。比如,皮肤上的毛孔和褶皱。
-
法线贴图:法线贴图就是在原物体的凹凸表面的每个点上均作法线,通过RGB颜色通道来标记法线的方向,你可以把它理解成与原凹凸表面平行的另一个不同的表面,但实际上它又只是一个光滑的平面。对于视觉效果而言,它的效率比原有的凹凸表面更高,若在特定位置上应用光源,可以让细节程度较低的表面生成高细节程度的精确光照方向和反射效果。(百度百科)
如何抉择:小细节用法线贴图或者凹凸贴图,大的轮廓使用置换贴图。
5.最后
claygl vs three
dead game dota2 based on claygl example
dead game dota2 based on claygl code 里面有英雄模型数据哟
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!