作者 | 若离
2021年初,阿里云智能IoT团队正式提出数字空间计划,旨在通过2.5D图形技术与数据可视化解决行业应用开发成本高,周期长,体验差的问题。本文将从前端技术的角度讨论2.5D技术在云组态搭建场景的技术探索与思考。
什么是2.5D技术?
2.5D就是通过二维的元素来呈现三维的效果。
2.5D技术在早期的游戏中应用非常广泛,而且深深扎根在每个玩家的心中。
譬如感动男孩子的第一款RTS游戏《红色警戒》,第一款全民国产RPG《仙剑奇侠传》,第一款MMORPG游戏《大话西游》,TGA2017最佳移动游戏《纪念碑谷》,甚至一些经久不衰的游戏如《魔兽争霸》《星际争霸》,系兄弟就来砍我的《贪玩蓝月》,以及阿里巴巴出品国民游戏《三国志战略版》都用到了2.5D技术。
《纪念碑谷》
lsometric Game 《红色警戒》
众所周知,我们智人是一种三维生物,但智人的眼睛又是一种二维感知器官。当人类的肉眼看到具有长、宽、高特征的图形时会主观认为这个物体是真实的,所以在早期计算机性能不足时开发者会使用二维图形来模拟三维场景,在保证性能的同时尽量使玩家具有更高的视觉体验。
简单来说,2.5D就是指2D图像通过等轴视角等技巧模拟出3D的纵深感,2.5D本质上和2D是一回事。(当然有的游戏是用3D技术来模拟2.5D,说的就是你《王者荣耀》!)
为什么是 2.5D ?
机智的你会问了:“都2021年了,谁还用2.5D?”,非也!在计算机性能溢出的今天3D确实已经在许多领域替代了2.5D,但2.5D却有其新的阵地,工业互联网(行业互联网)。
今天大家都在做行业互联网转型,最难做的是什么?工业互联网。工业互联网是360行的互联网,纺织和采掘的工业互联网就完全不同。其中能找到一些共性,譬如获取数据的方式,但是大家使用数据的方式完全不同。—— 逍遥子
讲一个故事,举一个栗子🌰
我们对接了一家做水处理的客户,客户希望将按照IoT规范将他们已有的水处理可视化应用做视觉升级,在保留组态间物理位置关系的同时保证组态数据的实时展示。于是UED小姐姐精心为客户的程序做了一次“整容”。
客户看了直呼好家伙,同时反馈他们有大概上百个场景需要搭建,所以他们要自由编辑场景中物体的布局,妥妥拽拽就能做好一个页面,不要花很多钱请设计师和工程师。而且工厂的电脑配置普遍不太高,不要画面整的卡卡的,但是效果不能打折扣。
成本很低,性能要高,效果还要酷炫,还要自定义。嗯。。这活没法干了.jpg 😿
但是仔细想一想,其实工业互联网的大多数应用场景都是这样的。
行业互联网应用现状
行业互联网与消费互联网最直观的差异就是用户群体。在传统的消费互联网中人类的情感是相通的,一款优秀的应用能够迅速聚集成千上万的用户,然而行业互联网则是各行各业的互联网,不同行业之间业务差异很大,即使在同一家企业,不同业务场景对于应用的需求也是千差万别。这就导致了下面这些问题:
开发者视角
- 成本高:开发成本高,行业模型绘制复杂,不同行业的模型逻辑差异很大。反复低效开发
- 不专业:行业线缺少专业的前端组件,基于通用组件二次开发,耗时低效
- 变化多:需求多变,行业需求针对性强,变化多,开发收益低
- 用户少:一款应用能覆盖到的用户往往只有几十到数百人,换一个场景就会水土不服。
用户视角
- 不好用:通用型的软件配置项太多,学习成本很高。酷炫的功能页面卡顿,用户体验差,
- 用不起:投入产出比不高,开发周期长,我还是用Excel方便
- 没啥用:一两个大屏汇报时候用一下,覆盖不到太多应用场景。没有解决实际问题
目前中国有拥有41个工业大类、207个中类、666个小类,是世界上门类最齐全的工业体系。中国工业和信息化部遴选出了305个智能制造试点示范项目,涉及92个行业类别,覆盖全国境内所有省(自治区、直辖市)
数据来源:
《2018年中国工业软件行业发展规模、未来发展趋势及行业发展前景分析》
《2017年中国中小工业企业运行报告》
上面两组数据能很好的反应工业软件的现状,占据大多数的小型企业并没有专业的工业软件来支撑他们进行信息化乃至智能化的升级,而且开发商也很难有动力针对这些中小型企业来做定制化的开发。
这里我提一个不成熟的想法:智能制造背后的逻辑其实是第四次工业革命。纵观前三次工业革命(蒸汽机,电力,自动化)都是一项普及到各行各业的技术推动了工业革命,这种变化往往是自下而上,全员参与的。
然而从工业互联网发展的角度来看,小型企业很难有技术能力参与到智能化改造的浪潮中来,那么如何解决这个问题呢?我认为云组态+数字空间2.5D或许是一条值得尝试的道路。2.5D在云组态中的应用实践请参照第三章。
2.5D 在前端的实现
2.5D 实现思路
设计风格:轴测视图
2.5D是个并不是很严谨的偏口语化的表述,从图形学的角度来看2.5D有许多种形式。但我们通常所指的2.5D是isometric,也就是等轴视图(轴测图)
2.5D轴测图是一种单面投影图,在一个投影面上能同时反映出物体三个坐标面的形状,并接近于人们的视觉习惯,形象、逼真,富有立体感。舍弃近大远小的关系,竖直方向没有高度变化,只有水平方向按角度偏移。所以说2.5D并没有严格还原肉眼观察到的物理世界。
技术选型:Canvas
作为一名前端工程师,面对这样的需求马上会想到如下解法:Css+js,SVG,Canvas,让我们依次进行尝试。怕你太长不看,这里给出结论:最终我们选用了Canvas来实现2.5D效果。
Css+JS
transform:rotate - 五面法:依次绘制模型5个面,分别为每个面填充颜色
毕竟“这个世界是方块组成的——Minecraft”,最简单的实现思路就是提前绘制好模型的五个面,通过transform: rotate()
来调整各面角度,并将其拼接起来即可。
篇幅所限:仅附上Pseudocode
#platform {
width: 500px;
height: 500px;
transform: rotateX(20deg) rotateZ(30deg) rotateY(0deg);
}
#platform b {
height: 100%;
background-color: rgba(176, 208, 223, 0.9);
transform: rotateX(90deg);
transform-origin: 0 0;
}
#platform b > b {
background-color: rgba(128, 174, 197, 0.9);
transform: rotateY(90deg);
}
...
/**
* 在平台创建新的物品块
* 采用b标签,可以模拟3D效果
* @param platform 模型所在平台
* @param height 模型高度
*/
function CreateCube(platform, height, top, left, name, w, h) {
var cube = document.createElement("div");
if (name === null) {
} else {
cube.id = name;
}
cube.style.height = height + "px";
cube.style.top = top + "px";
cube.style.left = left + "px";
platform.appendChild(cube);
var b1 = document.createElement("b");
b1.style.backgroundImage = "url(./images/steve3.jpg)";
b1.style.backgroundSize = "100% 100%";
b1.style.width = w + "px";
cube.appendChild(b1);
var b2 = document.createElement("b");
b2.style.width = h + "px";
b2.style.backgroundImage = "url(./images/steve1.jpg)";
b2.style.backgroundSize = "100% 100%";
b1.appendChild(b2);
var b3 = document.createElement("b");
b3.style.width = w + "px";
b3.style.backgroundImage = "url(./images/steve.jpg)";
b3.style.backgroundSize = "100% 100%";
b2.appendChild(b3);
var b4 = document.createElement("b");
b4.style.width = h + "px";
b4.style.backgroundImage = "url(./images/steve2.jpg)";
b4.style.backgroundSize = "100% 100%";
b3.appendChild(b4);
var i = document.createElement("i");
i.style.width = h + "px";
i.style.height = w + "px";
i.style.backgroundImage = "url(./images/steve4.jpg)";
i.style.backgroundSize = "100% 100%";
b4.appendChild(i);
}
这样我们调用CreateCube(platform, 300, 0, 0, null, 100, 100);
就可以生成一个具有五面的图形,修改css中platform 视角角度还能模拟出3D的效果。
- perspective(透视)+gradient(渐变):通过perspective来模拟出物体阴影,使用gradient模拟光源,再加上一定的视角偏移也能做出比较好的立体效果。
Pseudocode:
.stage {
perspective: 1200px;
perspective-origin: 50% 50%;
}
.circleWithLight {
display: block;
border-radius: 100%;
height: 100%;
width: 100%;
/* 径向渐变 */
background: radial-gradient(circle at 100px 100px, #5cabff, #000);
}
.circle3D .shadow {
position: absolute;
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.1) 40%, rgba(0, 0, 0, 0) 50%);
transform: rotateX(90deg) translateZ(-150px);
z-index: -1;
}
最初我觉得CSS能解决一切,毕竟原生Dom+css的开发简单调试方便,做好样式规范和封装即可。但后来我发现自己还是太年轻,当业务场景复杂并产生一些交互时CSS操作显得非常笨重,需要反复花大量的时间做样式调整。
SVG
SVG无疑是一种自定义能力非常强的方案,SVG 基于 XML,这意味着 SVG DOM中的每个元素都是可用的。我们可以很方便为某个元素附加 JavaScript事件。理论上Svg的path方法能够优雅的实现所有我们想要的效果,譬如2020年美国大选的实时地图就是通过SVG技术实现的。
但是Svg的开发成本明显较高(看看这密密麻麻的代码),即使采用Canvas2Svg或者Picture2Svg仍然大量需要设计师去介入。2.5D最主要的目的是降低成本,Svg Abandon!(终于有场合使用我最熟悉的单词)
Pseudocode:
<div id='wrap'>
<div class='row'>
<div class='cell'>
<div class='inner'>
<svg data-name='Layer 1' viewbox='0 0 800 800' xmlns='http://www.w3.org/2000/svg'>
<path class='squig' d='M 0 400 Q 120 400 120 500 L 120 660 Q 120 760 180 760 Q 240 760 240 660 L 240 140 Q 240 40 300 40 Q 360 40 360 140 L 360 660 Q 360 760 420 760 Q 480 760 480 660 L 480 140 Q 480 40 540 40 Q 600 40 600 140 L 600 660 Q 600 760 660 760 Q 720 760 720 660 L 720 480 Q 720 400 800 400 '></path>
</svg>
</div>
</div>
<div class='cell'>
<div class='inner'>
<svg data-name='Layer 1' viewbox='0 0 800 800' xmlns='http://www.w3.org/2000/svg'>
<path class='squig' d='M 0 400 Q 120 400 120 500 L 120 660 Q 120 760 180 760 Q 240 760 240 660 L 240 140 Q 240 40 300 40 Q 360 40 360 140 L 360 660 Q 360 760 420 760 Q 480 760 480 660 L 480 140 Q 480 40 540 40 Q 600 40 600 140 L 600 660 Q 600 760 660 760 Q 720 760 720 660 L 720 480 Q 720 400 800 400 '></path>
</svg>
</div>
</div>
<div class='cell'>
<div class='inner'>
<svg data-name='Layer 1' viewbox='0 0 800 800' xmlns='http://www.w3.org/2000/svg'>
<path class='squig' d='M 0 400 Q 120 400 120 500 L 120 660 Q 120 760 180 760 Q 240 760 240 660 L 240 140 Q 240 40 300 40 Q 360 40 360 140 L 360 660 Q 360 760 420 760 Q 480 760 480 660 L 480 140 Q 480 40 540 40 Q 600 40 600 140 L 600 660 Q 600 760 660 760 Q 720 760 720 660 L 720 480 Q 720 400 800 400 '></path>
</svg>
</div>
</div>
Canvas
Canvas是使用JavaScript 在网页上绘制2D图像的技术,我们可以通过多种方法使用Canvas 元素绘制路径、矩形、圆形、字符以及添加图像。Canvas的渲染特效不需要复杂的层叠运算,只需要输出最终的渲染结果,同时Canvas可以调用GPU运算,所以效率和性能更高,至于Canvas的事件绑定,开发工程化建设也已经有很多开源项目。
抛开开发效率,性能这些客观因素不谈,站在业务的角度上Canvas在业界已经有很多成熟的编辑器案例,生态建设已经比较成熟。许多在其他技术方案下比较复杂的效果(如影子,流动)在Canvas中用一两个配置就能实现。所以最终数字空间也选用了Canvas。
2.5D模型与场景搭建
那么如何来实现一个由轴测图构成的2.5D Canvas场景呢?我们尝试了三面法,五面法,顶点法,这些方法似乎都违背了我们做2.5D场景的初衷:降低成本。为了实现2.5D这种“折中”的效果,反而需要设计师花费大量精力来Flow这种效果。
有没有方案能让用户随手拿来一张图片就变成2.5D效果呢?
图形嵌入法
最终我们选择了最“low”也是最简单的方法,那就是在Canvas中嵌入提前绘制好的2.5D图片。这样在技术侧就不需要关心如何绘制2.5D效果,而是把注意力集中在如何优化2.5D组件的阴影,动效,交互中。同时为了提高2.5D物料的产出效率,我们可以分“三步走”。
- 阶段一:将3D模型在固定角度截图生成2.5D物料
- 阶段二:2.5D Generator - 一个将平面图片转为2.5D图片的ps插件
相关视频:
https://www.bilibili.com/video/BV1Q54y1q7oy?from=search&seid=3679341601769593977
https://www.bilibili.com/video/BV1Xf4y1q78h?from=search&seid=3679341601769593977
https://player.bilibili.com/player.html?bvid=BV1Xf4y1q78h
- 阶段三:实现图像2D→2.5D算法
类似2.5D Generator ,前端也可以通过计算2D图片的顶点,生成2.5D轴侧图。该算法正在研究中,先挖个坑,以后补上。
实现2.5D效果的其他尝试
- 三面法:通过绘制图形的三面构成模型。
- 五面法:依次绘制模型5个面,分别为每个面填充颜色
- 顶点法:绘制模型各顶点,通过填充顶点间区域上色
Canvas 技术选型
结论:经过大量的测试(每个框架都写了一个demo)最后选定Konva作为数字空间2.5D的基础库,因为篇幅所限没办法一一描述,再挖一个坑,以后来聊一聊目前市面上的Canvas库。
2.5D技术在云组态中的应用与探索
接下来分享2.5D技术在业务中落地的尝试。
随着云技术的发展,云托管具有低成本、高可靠、高性能、易运维的优势,强大的数据处理能力可以更好地对设备数据进行分析和可视化展示。设备上云已经越来越成为工业从业者的共识。云组态本质上就是企业数字化转型的一种途径,如果说数字化转型的基本逻辑是:采集→传输→存储→计算→应用。那么2.5D技术解决的数字化云组态的最后一个环节。
技术框架
如前文所述,行业数据的使用场景是多种多样的,即使同一家企业,不同的部门对于相同一组数据的使用方式都是不同的。譬如都是一组生产数据,品质工程师更关注产品良率而设备工程师关注设备稼动率。
那么作为一个行业应用搭建技术方案,我们只专注于2.5D云组态的展现形式,交互形式和数据响应,至于数据的处理逻辑则是以插件的形式由各行业项目定制化开发。
云组态开发的实践
工业场景组件设计
1、模型组件
模型组件是2.5D场景最基本的组成部分,其本质上是一个Image组件,着重需要解决的问题是image跨域和大量图片在Canvas中的性能表现。
Image跨域:Pseudocode
module.exports = function useImage(url, crossOrigin) {
var res = React.useState(defaultState);
var image = res[0].image;
var status = res[0].status;
var setState = res[1];
React.useEffect(
function () {
if (!url) return;
var img = document.createElement('img');
function onload() {
setState({ image: img, status: 'loaded' });
}
function onerror() {
setState({ image: undefined, status: 'failed' });
}
img.addEventListener('load', onload);
img.addEventListener('error', onerror);
crossOrigin && (img.crossOrigin = crossOrigin);
img.src = url;
return function cleanup() {
img.removeEventListener('load', onload);
img.removeEventListener('error', onerror);
setState(defaultState);
};
},
[url, crossOrigin]
);
// return array because it it better to use in case of several useImage hooks
// const [background, backgroundStatus] = useImage(url1);
// const [patter] = useImage(url2);
return [image, status];
};
- Image重新渲染缓存
这里调用了Konva库的batchDraw()方法
React.useEffect(() => {
// you many need to reapply cache on some props changes like shadow, stroke, etc.
shapeRef.current.cache();
shapeRef.current.getLayer().batchDraw();
// }
}, [img, filterOptions, { ...shadow }]);
2、管道组件
管道在工业场景中是最常见的组件,在2.5D场景下重点需要考虑的问题是管道绘制过程中角度的补正,水管水流效果的实现。
- 补正算法 Pseudocode
/**
* @name 计算两点之间角度Angle
* @param {object} p1
* @param {object} p2
*/
function getAngle(p1, p2, mode = 'fixed', perspective = 58) {
var diff_x = p2.x - p1.x;
var diff_y = p2.y - p1.y;
//返回角度,不是弧度
let angle = (360 * Math.atan(diff_y / diff_x)) / (2 * Math.PI);
//首先判断连线所处象限,原点为p1
let quadrant = 0; //定义象限
if (p2.x < p1.x && p2.y < p1.y) {
quadrant = 1;
} else if (p2.x > p1.x && p2.y < p1.y) {
quadrant = 2;
} else if (p2.x > p1.x && p2.y > p1.y) {
quadrant = 3;
} else if (p2.x < p1.x && p2.y > p1.y) {
quadrant = 4;
}
let feedBack = 0;
let fixedAngle = -0;
switch (quadrant) {
case 1:
{
angle = angle + 90;
fixedAngle = angle > 165 ? 180 : 180 - perspective;
}
break;
case 2:
{
angle = angle - 90;
fixedAngle = angle < -165 ? 180 : perspective - 180;
}
break;
case 3:
{
angle = angle - 90;
fixedAngle = angle > -15 ? 0 : -perspective;
}
break;
case 4:
{
angle = angle + 90;
fixedAngle = angle < 15 ? 0 : perspective;
}
break;
}
return fixedAngle;
}
- 管道水流效果实现
为了尽量还原真实管道中水流的动态效果,我们尝试了管道材质(通过贴图)、使用Script模拟管道动画、使用Rect模拟管道、用Line模拟管道,但这些方案都有各自的不足,最后通过重写CanvasPath来实现,我们将管道分为三层:管道外壁,管道内壁,管道水流。通过动态修改管道水流的Dash Offset就能实现水流效果
sceneFunc={function(ctx, shape) {
ctx.globalAlpha = 0.3;
ctx.shadowColor = shadow.color;
ctx.shadowOffsetX = shadow.offsetX;
ctx.shadowOffsetY = shadow.offsetY;
ctx.shadowBlur = shadow.blur;
ctx.stroke();
ctx.lineCap = 'round';
ctx.lineJoin = 'bevel';
//绘制三层水流
ctx.strokeStyle = '#C7E7F7';
ctx.lineWidth = 14;
drawShape(ctx, points, false);
ctx.stroke();
ctx.strokeStyle = '#888888';
ctx.lineWidth = 12;
drawShape(ctx, points, false);
ctx.stroke();
ctx.strokeStyle = '#094FE6';
ctx.setLineDash([5 * 0.3, 5]);
//处理水流效果
const t = time;
let l = 0;
for (let i = 1; i < points.length; i++) {
const a = points[i];
const b = points[i - 1];
l = l + Math.hypot(a[0] - b[0], a[1] - b[1]);
}
ctx.lineWidth = 10;
ctx.setLineDash([l * 0.8, l]);
ctx.lineDashOffset = mapRange(t, 0, 1, 0, -l * 0.7);
drawShape(ctx, points, false);
ctx.stroke();
// (!) Konva specific method, it is very important
// it will apply are required styles
ctx.fillStrokeShape(shape);
}
动态效果实现
Canvas的Filter结合动画可以覆盖多种业务场景。因为篇幅所限,具体的动态效果实现暂且不表。
在2.5D技术实践过程中有很多收获与总结,譬如组件设计,事件系统,数据响应,动态效果等,因为篇幅所限无法一一分享。
结语
在今天,工业4.0,智能制造的概念早已深入人心。然而大多数互联网企业对于行业应用的开发思路仍难免陷入到一款应用服务大多数人的思维定式中。行业互联网中不同的用户视角,复杂多变的应用场景都使“定制化”成为行业应用开发无法忽略的问题。在行业互联网中,只有用户自己开发的应用才是最符合其需求的应用。
如果说工业4.0是一次工业革命,那么这势必是自下而上的,影响到所有参与者的革命。