WebGL可视化基础(二)
想要学好可视化,我们要建立一套与各个图形系统无关联的/简单的基于向量和矩阵运算的数学体系,用它来描述所有的几何图形信息。也就是如何建立一套描述几何图形信息的数学体系,以及如何用这个体系来解决我们的可视化图形呈现的问题。
坐标系和坐标映射
HTML采用的是窗口坐标系,以参考对象(参考对象通常是最接近图形元素的position非static的元素)的元素盒子左上角为坐标原点。x轴向右,y轴向下,坐标值对应像素值。
SVG采用的是视区盒子(viewBox)坐标系。这个坐标系在默认情况下,是以svg根元素左上角为坐标原点,x轴向右,y轴向下,svg根元素右下角坐标为它的像素宽高值。如果设置了viewBox 属性,那么svg根元素左上角为viewBox的前来两个值,右下角为viewBox的后两个值。
Canvas采用以画布左上角为坐标原点,右下角坐标值为Canvas的画布宽高值。
WebGL的坐标系比较特殊,是一个三位坐标系。它默认以画布正中间为坐标原点,x轴朝右,y轴朝上,z轴朝外,x轴,y轴画布中范围是 -1 到 1。
为了方便图形,经常需要对坐标系进行转换。因为这四个坐标系都死直角坐标系,所以它们可以很方便地相互转化。其中,HTML/SVG和Canvas 都提供了transform的API能够帮助我们很容易地转换坐标系。而WebGL 不提供 transform的API,但我们可以在shader里做矩阵运算来实现坐标转换。
Canvas实现坐标系转换
之所以要实现坐标系的转换,是因为当我们绘图时,需要知道各个顶点的坐标,如果我们要花费时间在坐标换算上,这会非常的不方便。
首先,我们通过translate变换将Canvas画布的坐标原点,从左上角(0,0)点移动至(256,256)位置,即画布的底边上的中点位置,接着移动了原点后新的坐标为参照,通过scale(1,-1)将y轴向下的部分,即y > 0 的部分沿x轴反转180度,这样坐标系就变成以后画布底边中点为原点,x轴向右,y轴向上的坐标系了。
执行了坐标变换,也就是让坐标系原点在中间之后,我们就可以更方便/直观的计算出几个图形元素的坐标了。
采用坐标变换后,它能够简化计算量,这不仅让代码更容易理解,也可以节省CPU运算的时间。
如何用向量来描述点和线段?
v.length = function(){return Math.hypot(this.x, this.y)}
v.dir = function(){return Math.atan2(this.x, this.y)}
v.x = v.length * Math.cos(v.dir)
v.y = v.length * Math.sin(v.dir)
向量点乘(内积)和叉乘(外积、向量积)概念及几何意义解读
https://blog.youkuaiyun.com/dcrmg/article/details/52416832
如何用向量和参数方程描述曲线?
向量
首先,我们用向量绘制折线的方法来绘制正多边形,我们定义一个函数 regularShape,代码如下:
function regularShape(edges = 3, x, y, step) {
const ret = []
const delta = Math.PI * (1 - (edges - 2) / edges);
let p = new Vector2D(x, y);
const dir = new Vector2D(step, 0);
ret.push(p);
for(let i = 0; i < edges; i++) {
p = p.copy().add(dir.rotate(delta));
ret.push(p);
}
return ret;
}
我们在regularShape 函数中,给定边数 edges,起点x,y一条边的长度step,就可以绘制一个正多边形了。也就是通过 rotate 旋转向量,然后通过向量加法来计算顶点位置。
具体来说就是,定义初始化为new Vector2D(x, y),初始方向为 x 轴方向new Vector2D(step, 0)。然后循环计算正多边形的顶点位置,也就是初始点开始,每次将方向向量旋转delta角度,delta角度是根据正多边形内角公式计算出来的。最后,我们将当前点和方向向量相加,就得到下一个顶点坐标了。
draw(regularShape(3, 128, 128, 100)) // 绘制三角形
draw(regularShape(6, -64, 128, 50)) // 绘制六边形
draw(regularShape(11, -64, -64, 30)) // 绘制十一边形
draw(regularShape(60, 128, -64, 6)) // 绘制六十边形
当我们将多边形的边数设置非常多时,这个图形就会接近圆。所以,只要利用regularShape函数,将多边形的边数设置得很大,我们就可以绘制出圆形了,也就是利用极限的思维,获取一个近似圆形的多边形。
但是regularShape定义边数,起点,一条边的长度就和我们通常绘制圆的使用习惯不符。一般绘制圆为定义边数,中心,半径。
其次,regularShape无法绘制椭圆,抛物线,贝塞尔曲线等其他曲线。
曲线方程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpyuFpFu-1649255314206)(/Users/longyusheng/Downloads/下载 (5)].jpeg)
const TAU_SEGMENTS = 60;
const TAU = Math.PI * 2;
function arc(x0, y0, radius, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const ret = ang === TAU ? [] : [[x0, y0]];
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const x = x0 + radius * Math.cos(startAng + ang * i / segments);
const y = y0 + radius * Math.sin(startAng + ang * i / segments);
ret.push([x, y])
}
return ret;
}
draw(arc(0, 0, 100));
画圆锥曲线
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLzCqL5w-1649255314206)(/Users/longyusheng/Downloads/下载 (6)].jpeg)
画抛物线
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aV8dk6Z8-1649255314207)(/Users/longyusheng/Downloads/下载 (7)].jpeg)
我们只需要修改arc方法中对应的参数同样能够实现椭圆和抛物线的绘制。
const TAU_SEGMENTS = 60;
const TAU = Math.PI * 2;
function arc(x0, y0, radiusX, radiusY, startAng = 0, endAng = Math.PI * 2) {
const ang = Math.min(TAU, endAng - startAng);
const ret = ang === TAU ? [] : [[x0, y0]];
const segments = Math.round(TAU_SEGMENTS * ang / TAU);
for(let i = 0; i <= segments; i++) {
const x = x0 + radiusX * Math.cos(startAng + ang * i / segments);
const y = y0 + radiusY * Math.sin(startAng + ang * i / segments);
ret.push([x, y])
}
return ret;
}
draw(arc(0, 0, 100, 50));
const LINE_SEGMENTS = 60;
function parabola(x0, y0, p, min, max) {
const ret = [];
for(let i = 0; i <= LINE_SEGMENTS; i++) {
const s = i / 60;
const t = min * (1 - s) + max * s;
const x = x0 + 2 * p * t ** 2
const y = y0 + 2 * p * t;
ret.push([x, y]);
}
return ret;
}
draw(parabola(0, 0, 5.5, -10, 10))
参考文章
极客时间:跟月影学可视化