Canvas原生API(纯CPU)计算并渲染三维图
Canvas原生API(纯CPU)计算并渲染三维图
前端工程师学图形学:Games101 第三次作业
利用Canvas画三维中的三角形并使用超采样实现抗锯齿
最终完成功能
- Canvas 原生API实现三角形栅格化算法
- 实现 z-buffer 判断三角形先后关系
- 使用 super-sampling 处理 Anti-aliasing,也就是超采样实现抗锯齿
1 整体分析
本次实验中,首先需要进行矩阵变换,将初始传入的三角形经过变换后到规范立方体内,这需要进行三种变换。设一个点的坐标变换为(x, y, z) -> (x', y', z')
x' \\ y' \\ z' \\ 1
\end{bmatrix}
=
M_{presp} \times M_{view} \times M_{model} \times
\begin{bmatrix}
x \\ y \\ z \\ 1
\end{bmatrix}
\]
每个矩阵的求解在之前的博客中都有讲解图形学 旋转与投影矩阵—2 - 知乎 (zhihu.com),这是其中一篇可供参考,因此,投影矩阵,视图矩阵和模型矩阵这里不再求解。现状,变换矩阵已经知道,现状需要将转换后的矩阵进行光栅化,在光栅化时,需要遍历屏幕上的每个像素点进行判断,该点是否在三角形内,如果在,则渲染,由于本文采用的 Canvas 进行渲染,因此需要对 Canvas 上的每个像素点进行判断。
光栅化完成后,可得到一个充满颜色的三角形,如果渲染多个三角形,会产生覆盖现象,这个时候就需要判断深度,因此我们需要维护一个深度缓冲的数组,这个数组的大小为 canvas 的 width*height。当渲染后面的三角形时,首先判断该像素的当前深度是否小于预渲染像素的深度,如果小于,则渲染,否则,不进行处理。
上述完成后,会得到一些一个带锯齿的三角形,为了解决锯齿问题,这里进行了超采样,即让一个像素点平分为 9 块正方形区域,看九块区域有多少在三角形内,占比情况,凭占比量设置该像素的颜色,最终完成抗锯齿的功能。
总结,完成该实验的步骤如下
- 矩阵变换,投影,视图,模型变换
- 光栅化,使用 Canvas 原生 Api 画颜色
- 抗锯齿,超采样实现,将一个像素点分为 9 个正方形
2 代码分析
第一步:矩阵变换函数
// 变换函数
function getFinalPosition(position){
const finalPosition = new THREE.Vector4().set(
position.x,
position.y,
position.z,
1
).applyMatrix4(perspMatrix).applyMatrix4(viewMatrix);
finalPosition.set(
finalPosition.x/finalPosition.w,
finalPosition.y/finalPosition.w,
finalPosition.z/finalPosition.w,
1
)
return finalPosition;
}
输入三角形的坐标即可得到转换后的最终坐标,为了简单使用,这里没有使用到模型矩阵,仅仅用到了投影矩阵和视图矩阵。经过转换后,三角形坐标 x,y,z 都被规范到 [-1, 1] 之间了
第二步:将像素坐标转换为屏幕坐标
屏幕空间内,像素是从 (0, 0) 到 (width-1, height-1),渲染的范围为 (0, 0) 到 (width, height),width 和 height 是 Canvas DOM 的宽和高。注意:像素是一个一个方块,如下图所示。
由此可得,规范立方体到屏幕空间的坐标变换矩阵为
=
\begin{bmatrix}
\frac{width}{2} & 0 & 0 & \frac{width}{2} \\
0 & \frac{height}{2} & 0 & \frac{height}{2} \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\]
代码如下
// 转换成屏幕坐标
function transScreen(positions, width, height){
const MViewPort = new THREE.Matrix4();
MViewPort.set(
width/2, 0, 0, width/2,
0, -height/2, 0, height/2,
0, 0, 1, 0,
0, 0, 0, 1
)
positions.forEach(vec => vec.applyMatrix4(MViewPort));
}
第三步:光栅化
光栅化:遍历相应像素点,判断该点是否在三角形内,在的话才继续处理,遍历范围是包围三角形最大的矩形盒。
求得三个顶点的宽高的最大最小值即可求出这个包围盒,分别为 minX, minY, maxX, maxY,开始遍历判断
const ctx = canvas.getContext('2d');
...
// 1. 遍历包围盒的每个像素
for (let i = Math.round(minX); i < maxX; i++) {
for (let j = Math.round(minY); j < maxY; j++) {
...
// 2. 判断像素是否在三角形内
if((count=getInner(point1, point2, point3, pixel))!==0){
...
// 3. 创建颜色
switch (type) {
case 1:
data[0] = redColor;
data[1] = greenColor;
data[2] = blueColor;
data[3] = 透明度;
break;
case 2:
data[0] = redColor;
data[1] = greenColor;
data[2] = blueColor;
data[3] = 透明度;
break;
}
// 4. 赋予相应像素点颜色
ctx.putImageData(myImageData, i, j);
}
}
}
运行上述程序后,Canvas 绘画单个三角形的工作便完成了。
我们需要维护一个深度数组,用来存储当前像素的深度
const z_buffer = []
for (let i = 0; i < height; i++) {
const arr = [];
z_buffer.push(arr)
for (let j = 0; j < width; j++) {
arr.push(-Number.MAX_VALUE)
}
}
设置每个数字为无穷远,代表后续的每个三角形都比其像素点近,如果点在三角形内,判断当前深度并进行赋值
// getZ 表示获取欲渲染像素点的深度,z_buffer 存储当前像素点深度
const z = getZ(i+0.5, j+0.5);
if(z<z_buffer[j][i]){
// console.log('success');
continue;
}
z_buffer[j][i] = z;
第四步:抗锯齿
将一个像素点分为 9 份相同大小的正方形,判断有多少份正方形在三角形内,最后凭占比赋予颜色
// 获得一个像素点分成 9 份正方形后,在三角形内的个数
function getInner(point1, point2, point3, pixel){
let extend = {x:0, y:0, index: 0}
for (let i = 1/6; i < 1; i+=1/3) {
for (let j = 1/6; j < 1; j+=1/3) {
extend.x = i;
extend.y = j;
if(isInner(point1, point2, point3, pixel, extend)) extend.index++;
}
}
// 判断当前正方形是否在三角形内
function isInner(point1, point2, point3, pixel, extend){
pixel.x += extend.x;
pixel.y += extend.y;
const ab = new THREE.Vector3().subVectors(point2, point1);
const bx = new THREE.Vector3().subVectors(pixel, point2);
const direct1 = new THREE.Vector3().crossVectors(ab, bx);
const bc = new THREE.Vector3().subVectors(point3, point2);
const cx = new THREE.Vector3().subVectors(pixel, point3);
const direct2 = new THREE.Vector3().crossVectors(bc, cx);
const ca = new THREE.Vector3().subVectors(point1, point3);
const ax = new THREE.Vector3().subVectors(pixel, point1);
const direct3 = new THREE.Vector3().crossVectors(ca, ax);
const f1 = direct1.dot(direct2);
const f2 = direct2.dot(direct3);
return Math.sign(f1) === 1 && Math.sign(f2) === 1;
}
return extend.index;
}
将像素点平均分成九份后,每份都为正方形,找出正方形中心,判断该中心是否在正方形内,最后总结出在三角形内的正方形个数,最后赋予颜色.
count=getInner(point1, point2, point3, pixel);
const rat = count/9;
switch (type) {
case 1:
data[0] = 255 * rat + oriData[0] * (1-rat);
data[1] = oriData[1] * (1-rat);
data[2] = oriData[2] * (1-rat);
data[3] = 255 * rat + oriData[3] * (1-rat);
break;
case 2:
data[0] = oriData[0] * (1-rat);
data[1] =255 * rat + oriData[1] * (1-rat);
data[2] =255 * rat + oriData[2] * (1-rat);
data[3] =255 * rat + oriData[3] * (1-rat);
break;
}
3. 总结
使用 Canvas 原生 API 实现三维图形的光栅化,能够加强我们都图形学坐标转换的整体印象,能使我们了解基本原理,对我们理解游戏等三维引擎的底层原理有很大的帮助。
我这完整代码没有整理,不太好看,就不放出来了,需要源码交流的可以私聊,如果觉得有用,可以点个赞哦。
Canvas原生API(纯CPU)计算并渲染三维图的更多相关文章
- 把图片存储 canvas原生API转成base64
1.LocalStorage有什么用? 2.LocalStorage的普通用法以及如何存储图片. 首先介绍下什么是LocalStorage 它是HTML5的一种最新储存技术.但它只能储存字符串.以前的 ...
- Zookeeper系列三:Zookeeper客户端的使用(Zookeeper原生API如何进行调用、ZKClient、Curator)和Zookeeper会话
一.Zookeeper原生API如何进行调用 准备工作: 首先在新建一个maven项目ZK-Demo,然后在pom.xml里面引入zk的依赖 <dependency> <groupI ...
- 【转】NativeScript的工作原理:用JavaScript调用原生API实现跨平台
原文:https://blog.csdn.net/qq_21298703/article/details/44982547 -------------------------------------- ...
- 使用JavaScript调用手机平台上的原生API
我之前曾经写过一篇文章使用Cordova将您的前端JavaScript应用打包成手机原生应用,介绍了如何使用Cordova框架将您的用JavaScript和HTML开发的前端应用打包成某个手机平台(比 ...
- 前端未来趋势之原生API:Web Components
声明:未经允许,不得转载. Web Components 现世很久了,所以你可能听说过,甚至学习过,非常了解了.但是没关系,可以再重温一下,温故知新. 浏览器原生能力越来越强. js 曾经的 JQue ...
- html5 canvas常用api总结(三)--图像变换API
canvas的图像变换api,可以帮助我们更加方便的绘画出一些酷炫的效果,也可以用来制作动画.接下来将总结一下canvas的变换方法,文末有一个例子来更加深刻的了解和利用这几个api. 1.画布旋转a ...
- jQuery? 回归JavaScript原生API
如今技术日新月异,各类框架库也是层次不穷.即便当年漫山红遍的JQuery(让开发者write less, do more,So Perfect!!)如今也有被替代的大势.但JS原生API写法依旧:并且 ...
- 注解 @RequestParam,@RequestHeader,@CookieValue,Pojo,servlet原生API
1.@RequestParam 我们的超链接:<a href="springMvc/testRequestParam">testRequestParam</a&g ...
- (原) 2.1 Zookeeper原生API使用
本文为原创文章,转载请注明出处,谢谢 Zookeeper原生API使用 1.jar包引入,演示版本为3.4.6,非maven项目,可以下载jar包导入到项目中 <dependency> & ...
随机推荐
- String.split()与StringUtils.split()的区别
import com.sun.deploy.util.StringUtils; String s =",1,,2,3,4,,"; String[] split1 = s.split ...
- my41_主从延迟大排查
半同步复制 主库执行 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; SET GLOBAL rpl_semi_sync ...
- 【力扣】922. 按奇偶排序数组 II
给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数. 对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数:当 A[i] 为偶数时, i 也是偶数. 你可以返回任何满足上述条件的数组 ...
- CPU的中断
目录 一.简介 二.具体 方式 硬中断 软中断 中断切换 网卡中断 三.中断查看 一.简介 中断其实就是由硬件或软件所发送的一种称为IRQ(中断请求)的信号.中断允许让设备,如键盘,串口卡,并口等设备 ...
- CF1080A Petya and Origami 题解
Content 小 P 想给 \(n\) 位朋友各发一张邀请函,每张邀请函需要耗费 \(2\) 张红色纸,\(5\) 张绿色纸和 \(8\) 张蓝色纸.商店里面的纸是一堆一堆卖的,每堆里面有 \(k\ ...
- Python3 中bytes数据类型深入理解(ASCII码对照表)
bytes的来源 bytes 是 Python 3.x 新增的类型,在 Python 2.x 中是不存在的. bytes 的意思是"字节",以字节为单位存储数据.而一个字节二进制为 ...
- atexit模块介绍
atexit 模块介绍 python atexit 模块定义了一个 register 函数,用于在 python 解释器中注册一个退出函数,这个函数在解释器正常终止时自动执行,一般用来做一些资源清理的 ...
- SpringCloud微服务实战——搭建企业级开发框架(三十五):SpringCloud + Docker + k8s实现微服务集群打包部署-集群环境部署
一.集群环境规划配置 生产环境不要使用一主多从,要使用多主多从.这里使用三台主机进行测试一台Master(172.16.20.111),两台Node(172.16.20.112和172.16.20.1 ...
- JAVA中Base64和byte数组(byte[]) 相互转换
Base64转byte[] byte[] bytes = DatatypeConverter.parseBase64Binary("base64字符串"); byte[]转base ...
- 论文解读SDCN《Structural Deep Clustering Network》
前言 主体思想:深度聚类需要考虑数据内在信息以及结构信息. 考虑自身信息采用 基础的 Autoencoder ,考虑结构信息采用 GCN. 1.介绍 在现实中,将结构信息集成到深度聚类中通常需要解决以 ...