honmono

游戏和计算机图形学

0%

macos m1 从0构建Godot源码

官方文档:

https://docs.godotengine.org/zh_CN/stable/development/compiling/compiling_for_osx.html#doc-compiling-for-osx

1. 准备编译环境

在 macOS 下编译时,需要以下条件:

  1. Python 3.5+.
  2. SCons 3.0+ 构建系统.
  3. Xcode(或更轻量的 Xcode 命令行工具)。
  4. 可选——yasm(用于 WebM SIMD 优化)。

确保Xcode版本为14+. 如果不是, 通过下面的命令更新Xcode.

1
xcode-select --install

如果你使用Python pip 安装了低版本的sCons, 需要先卸载低版本的sCons.

1
pip uninstall scons

通过brew安装scons和yasm

1
brew install scons yasm

通过git clone godot源码, 选择一个稳定的分支(我这里使用的是4.1版本)

1
git clone git@github.com:godotengine/godot.git -b 4.1

2. 开始编译Godot

先检查scons是否正常, 进入godot的根目录, 执行以下命令.

1
scons platform=list

如果scons正常, 那么会输出支持的平台信息

1
2
3
4
5
scons: Reading SConscript files ...
The following platforms are available:
ios
macos

如果报错, 例如

1
scons SyntaxError: invalid syntax

这个时候需要检查scons版本是否正确, 建议卸载原有scons, 重新使用python3 或者 brew重新安装.

scons确认正常后, 执行下面的命令

1
scons platform=macos arch=arm64 --jobs=$(sysctl -n hw.logicalcpu)

如果报以下错误, 说明moltenvk没有安装

1
moltenvk sdk installation directory not found

通过brew安装moltenvk

1
brew install molten-vk

安装完毕后, 重新执行

1
scons platform=osx arch=arm64 --jobs=$(sysctl -n hw.logicalcpu)

如果一切正常, 执行完毕后, 在/bin目录会出现godot的可执行文件, 命令号执行后可以打开godot.


消除掉落算法, 不规则地图/固定格子

掉落消除算法很常见, 但是带格子固定, 或者是不规则地图的算法还是比较少见的.

这里提供一个比较万金油的, 可扩展的方法去解决不规则地图, 或者是有些格子是锁定的, 不参与掉落的情况.

1. 数据结构设计

1
2
3
4
5
6
7
8
9
10
class GridData {
// 被销毁了
public bool destroyed;
// 被固定了
public bool locked;
// 被预定了
public bool reserved;
// 类型, 枚举值
public GridType type;
}

对于destroyed, locked, reserved 值的不同, 我有三种不同的称呼

  1. 空格子, 即destroyed为true的格子.
  2. 动态格子, locked==false && destroyed == false;
  3. 被预定的格子. reserved == true;

地图使用二维数组表示, 对于不规则地图, 也是一个二维数组, 不参与掉落的格子设置为locked即可.

1
2
3
class Map {
public GridData[,] gridDatas;
}

2. 查找相邻同类型的格子

这个基本都类似, 通过递归的思想, 对于格子A, 先放入到查找队列List_A.

从List_A中取出一个格子, 上下左右遍历找到同类型的格子, 如果有, 记录这个新格子, 再将格子加入到List_A, 重复上面的步骤, 直到List_A里面没有格子.

为了加速这个过程, 避免重复, 一般还需要加入一个已检查的Set_A, 每检查一个格子, 判断格子是否在Set_A中, 如果有跳过这次检查, 如果没有, 将格子放入到Set_A中.

1
2
3
4
5
6
7
8
9
10
void _FindAllAdjacentGrids(int idx, GridColorType colorType, in List<int> adjacentGrids) {
var aroundGridIdxs = this._GetAroundGridIdxs(idx, colorType);
if (aroundGridIdxs.Count <= 0) return;

adjacentGrids.AddRange(aroundGridIdxs);

foreach (var aroundGridIdx in aroundGridIdxs) {
this._FindAllAdjacentGrids(aroundGridIdx, colorType, adjacentGrids);
}
}

3. 消除, 掉落和平移

首先在创建地图的二维数组时, 可以将行高扩大一倍, 也就是地图设计有n列m行, 那么就创建一个n * 2m的二维数组.

这么做有几个好处

  1. 不用担心在消除后, 没有对应的下落格子. 因为一次最多消除m行, 所有一定有足够的格子.
  2. 不用创建新的格子, 所有的掉落都通过交换的方式实现. 最终被消除的格子一定被交换到m行以上了.
  3. 销毁通过设置格子的destroyed为true. 掉落完成后, 对行数>=m的格子进行数据初始化, destroyed设置为false. 实现0GC完成掉落和生成.
1. 消除实现

对上一步获取到的格子, 遍历设置destroyed为true, 表示已经被消除.

2. 掉落实现

所有格子遵循两个个原则就是:

  1. 对与当前这个格子, 它是动态格子(不同格子的称呼请看1.数据结构设计), 并且它的下方是空格子, 那个这个格子参与掉落, 直到它下方不再是空格子.
  2. 掉落完成后, 遍历所有格子, 如果当前格子是动态格子, 并且它下面一个格子A也是动态格子, 那么判断A格子当前行, 左右是否有可达的空格子. (可达的空格子指: A格子到空格子之间全都是动态格子, 0个或多个).

对于2. 可以转化为

掉落完成后, 遍历所有格子, 如果当前格子是空格子, 那么判断左右是否有可达的动态格子, 这个动态格子的上面也是动态格子. 如果达成条件, 那么左右平移动态格子到填到空格子中.

具体实现方法:

掉落:

向下交换, 直到下面不是空格.

1
2
3
4
5
6
7
8
private int _DropDownGrid(int x, int y) {
var step = -1;
while (this.CheckIsEmptyGrid(x, y + step)) {
(this._gridDatas[x, y], this._gridDatas[x, y + step]) = (this._gridDatas[x, y + step], this._gridDatas[x, y]);
y = y + step;
}
return y;
}

平移:

和掉落类似, 找到格子A和空格子B, 将空格子B和相邻的格子交换, 直到交换到格子A.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void MoveHorizontalGrid(int startIdx, int endIdx, in List<MoveGridData> movingGrids) {
var startCol = this.GetPosition(startIdx).x;
var endCol = this.GetPosition(endIdx).x;
var row = this.GetPosition(startIdx).y;
if (startCol < endCol) {
// 右移
for (var x = startCol; x < endCol; x++) {
(this._gridDatas[x, row], this._gridDatas[x + 1, row]) = (this._gridDatas[x + 1, row], this._gridDatas[x, row]);

movingGrids.Add(new MoveGridData(new Position() {x = x + 1, y = row}, new Position() {x = x, y = row}, this._gridDatas));
}
} else {
// 左移
for (var x = startCol; x > endCol; x--) {
(this._gridDatas[x, row], this._gridDatas[x - 1, row]) = (this._gridDatas[x - 1, row], this._gridDatas[x, row]);
movingGrids.Add(new MoveGridData(new Position() {x = x - 1, y = row}, new Position() {x = x, y = row}, this._gridDatas));
}
}
}

平移完成后, 进行掉落, 掉落完成再进行平移, 直到没有新的掉落和平移, 那么说明格子全都处于稳定状态了.

1
2
3
4
5
6
7
8
9
10
11
public List<List<MoveGridData>> AdjustAllGrids() {
var allMovingGrids = new List<List<MoveGridData>>();
while (true) {
var dropingGrids = this._DropDownAllGrids();
var movingGrids = this._FillHorizontalEmptyGrids();
if (dropingGrids.Count <= 0 && movingGrids.Count <= 0) break;
allMovingGrids.Add(dropingGrids);
allMovingGrids.Add(movingGrids);
}
return allMovingGrids;
}

最终效果.

光线追踪实现

射线

image-20230525132930190

r(t) = o + td;

image-20230525133057206

贝塞尔曲面

四条贝塞尔曲线, 这四条贝塞尔曲线在T0时间的点, 作为控制点, 在得到一条新的贝塞尔曲线, 新贝塞尔曲线在T1时候的点, 我们就认为是曲面上的点

image-20230525171832262

球体

C为球体中心

image-20230606141609630

设P点位于球体表面上, 可得

image-20230606142227140

将P带入 射线方程 o+td的形式

image-20230606142333163

当小于r的平方时, 说明发生了碰撞

抗锯齿

image-20230606194748703image-20230606194833049

漫反射

image-20230606201258218 d3f094c2528e746a10a5f6d78384fb97 image-20230607150317959 image-20230607172030226 image-20230607181127863 image-20230607195138267

809D7D8A74AA122DE821C5D456117DCB

球体生成与渲染(细分法)

单纯生成球体的mesh很简单, 但是要保证mesh和uv对应, 并且在接缝处不出现问题还是会有一些麻烦的.

1. 细分算法

细分法很简单, 本质就是对一个三角形进行细分变成四个三角形

image-20230522162636334

如图(网图, 侵删), 在三角形的三条边上取中点, ab, ac, bc, 这三个点减去球心, 得到从球心指向ab, ac, bc 的向量, 在做归一化, 乘以球体半径, 得到最终的ab, ac, bc. 对这六个点, 可以得到四个三角形

  • ab, ac, bc
  • a, ab, ac
  • b, ab, bc
  • c, bc, ac

然后对这四个三角形重复上面的步骤, 直到越来越接近一个完美的曲面.

2. 球体实现

具体实现方法, 首先我们准备一个正二十面体, 从这个二十面体细分出球体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const double t = 1.61803;
this.vertices.Add(Vector4.Create(-1, t, 0, 1));
this.vertices.Add(Vector4.Create(1, t, 0, 1));

this.vertices.Add(Vector4.Create(-1, -t, 0, 1));
this.vertices.Add(Vector4.Create(1, -t, 0, 1));

this.vertices.Add(Vector4.Create(0, -1, t, 1));
this.vertices.Add(Vector4.Create(0, 1, t, 1));

this.vertices.Add(Vector4.Create(0, -1, -t, 1));
this.vertices.Add(Vector4.Create(0, 1, -t, 1));

this.vertices.Add(Vector4.Create(t, 0, -1, 1));
this.vertices.Add(Vector4.Create(t, 0, 1, 1));

this.vertices.Add(Vector4.Create(-t, 0, -1, 1));
this.vertices.Add(Vector4.Create(-t, 0, 1, 1));

var indexs = new int[] {
0, 11, 5,
0, 5, 1,
0, 1, 7,
0, 7, 10,
0, 10, 11,
1, 5, 9,
5, 11, 4,
11, 10, 2,
10, 7, 6,
7, 1, 8,
3, 9, 4,
3, 4, 2,
3, 2, 6,
3, 6, 8,
3, 8, 9,
4, 9, 5,
2, 4, 11,
6, 2, 10,
8, 6, 7,
9, 8, 1
};

第二步细分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (var i = 0; i < subdivisions; i++) {
var newIndices = new List<int>();
for (var j = 0; j < indexs.Length; j += 3) {
var v1 = indexs[j];
var v2 = indexs[j+1];
var v3 = indexs[j+2];

var a = this.GetMiddlePoint(v1, v2);
var b = this.GetMiddlePoint(v2, v3);
var c = this.GetMiddlePoint(v3, v1);

newIndices.AddRange(new int[] { v1, a, c });
newIndices.AddRange(new int[] { v2, b, a });
newIndices.AddRange(new int[] { v3, c, b });
newIndices.AddRange(new int[] { a, b, c });
}
indexs = newIndices.ToArray();
}
this.indices.AddRange(indexs);
1
2
3
4
5
6
7
8
// 获取两点的中点
private int GetMiddlePoint(int v1, int v2) {
var p1 = this.vertices[v1];
var p2 = this.vertices[v2];
var mid = p1.Add(p2).MulSelf(0.5).NormalizeSelf();
this.vertices.Add(mid);
return this.vertices.Count - 1;
}

这样我们就得到了一个可以设置任意细分次数的球体mesh.

3. 计算uv和法线

球体的uv, 我们可以理解为经纬度.

image-20230522164449728

对于纬度, 也就是上到下, 我们可以用a角表示

image-20230522164145915

对于经度, 也就是左到右, 图片有点问题, 点应该在曲线上, 但是没啥影响. 也可以用, a和b的tan角表示

image-20230522164612912

代码:

1
2
var u = Math.Atan2(vector4.x, vector4.z) / (2.0f * Math.PI) + 0.5f
var v = Math.Asin(vector4.y) / Math.PI + 0.5f)

法线更简单了, 球心到顶点的连线即为法线:

1
var normal = vec.Normalize();

4. 问题:

实现完发现了一个问题, 在接缝处会出现明显的纹理被压缩的画面.

image-20230522165021187 image-20230522164959582

可以看到, 在最左边到最右边的接缝区域, 三角形纹理采样有问题.

这是因为会出现有三角形的两个顶点的uv坐标是跨越了左右边缘的情况, 比如顶点A的uv.x是0.95, 顶点B的uv.x是0.1. 那么这个时候我们期望的uv差值应该是从0.95 -> 1.0 -> 0.1这个方向的, 但是实际情况确是0.95 -> 0.5 -> 0.1这个方向, 这个方向就几乎涵盖了整个纹理, 所以会出现在接缝区域的纹理被压缩的感觉.

这种情况话, 把顶点B的uv.x改为1.1也是没用的, 因为对于下一个三角形, 本来是0.1 ~ 0.3, 现在变成1.1 ~ 0.3, 方向还是错误的.

解决办法:

没有想到特别好的解决办法, 目前想的方法是在接缝区域的三角形多生成一个顶点, 还是上面的例子, 顶点A和顶点B, 在顶点B的位置多加一个顶点C, 顶点C的uv.x为1.1, 顶点A和顶点C位接缝处的三角形, 而顶点B为下一个三角形的顶点.

这样就可以解决接缝处uv采样错误的问题了.

手撸图形渲染器

1. 进展

github: https://github.com/kirikayakazuto/HRanderer-CShape

1. 渲染三角形

1280X1280

2. 渲染图片

1280X1280 (1)

3. 图片溶解效果

4. 正交投影

5. 透视投影

6. Phong光照模型渲染

7. 模型渲染

8. 抗锯齿 msaa

1280X1280 (2)1280X1280 (3)

9. 模型抗锯齿

10. 模版测试

11. 线段模式(Bresenham算法)

12. 点模式

13. 半透明混合

e365eca1-8328-4758-a6e5-14f09862545e

14. 球体渲染

15. 深度信息输出

2. 项目结构

图片1

1. 渲染管线对应实现

实现了GPU对应的渲染管线.

  • 顶点处理 & 顶点着色器(可编程)

  • 图元装配(只支持三角形)

  • 几何着色器(待实现)

  • 光栅化

  • 片段着色器(可编程)

  • 测试和混合

2. 其他能力

  • 背面剔除(已实现)

  • 视椎剔除(待实现)

  • 半透明物体渲染(简单实现)

1. 安装系统, 我一般选择CentOs 7.x

进入https://swas.console.aliyun.com/
类似的云服务器管理平台, 在购买或者购买后可以在重置系统中修改系统.

2. 修改防火墙/安全组

开放80, 443, 22端口, 分别是浏览器Http, Https, 和ssh的端口. 这个是最基本的端口, 可以先打开.
后面如果你自己部署了自己的服务, 也需要在这里打开对应的端口.

image-20230516140832335

3. 登录到服务器

简单的话可以直接使用云服务器平台的网页登录.

当然, 我们还是使用自己的工具登录. 首先打开22端口, 然后找到云服务器的密码.

mac可以直接使用终端

1
ssh -p 22 root@xxx.xxx.xxx.xxx

或者可以使用工具, 推荐nuoshell.

使用密码登录上后, 我们可以换成安全性更高的秘钥登录.

本地生成ssh秘钥对, 这个大家用git应该都生成了, 没有生成的可以使用ssh-keygen

1
$ ssh-keygen -t rsa

生成秘钥后, 将秘钥上传到服务器

1
$ ssh-copy-id -i ~/.ssh/id_rsa.pub root@xxx.xxx.xxx.xxx

之后就可以通过ssh直接登录了

1
ssh 'root@xxx.xxx.xxx.xxx'
4. 安装必要环境

首先更新系统包

1
sudo yum update

yum常用命令

1
2
3
4
5
6
// 安装
yum install git
// 卸载
yum remove git
// 查看已经安装的
yum list installed
  1. 安装nvm

    1
    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
    1
    2
    echo "source ~/.nvm/nvm.sh" >> ~/.bashrc
    source ~/.bashrc
  2. 安装nginx

    1
    2
    3
    4
    5
    6
    7
    yum install nginx

    // 配置文件目录
    /etc/nginx/nginx.conf

    // 重启nginx
    nginx -s reload
  3. nginx设置https

    首先到云服务平台下载ssl证书, 应该都有免费的一年的证书, 下载到本地后, 会有两个文件, 一个.key, 一个.pem

    将.pem文件的后缀改为.crt, 在服务器中创建一个文件夹ssl_certificate, 上传到该文件夹

    修改nginx配置

    主要修改三个位置, root, ssl_certificate, ssl_certificate_key;

    这里需要修改两个server, 可以直接修改后, copy我这个.

    第一个server是设置https的, 第二个server是把之前的http请求转到https中.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name _;
    root /usr/share/nginx/html/blog;

    ssl_certificate "/root/workspace/ssl_certificate/www.honmono.top.crt";
    ssl_certificate_key "/root/workspace/ssl_certificate/www.honmono.top.key";
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    error_page 404 /404.html;
    location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    }
    }

    server {
    listen 80;
    listen [::]:80;
    server_name honmono.top;
    rewrite ^(.*)$ https://$host$1 permanent;

    }
  4. 安装Shadowsocks

    https://www.linuxsss.com/oneclick/teddysun-shadowsocks-one-click-script/

    1
    2
    3
    4
    5
    6

    yum update -y && yum install wget -y && yum install curl -y

    chmod +x shadowsocks-all.sh

    ./shadowsocks-all.sh 2>&1 | tee shadowsocks-all.log
  5. 本地上传文件到远程服务器

    1
    scp /path/filename username@servername:/path   

Cocos Creator插件 之状态控制器(PropController)

思路来自fgui中的状态控制器

前言

做游戏难免有拼UI的时候, 而拼UI经常会有状态切换的情况.

比如一张卡牌, 有三个状态, 待开启, 进行中, 已结束. 每个状态会修改结点上的文本, 图片, 和显示/隐藏的切换,

平时都是通过代码去实现这些状态, 但是都很繁琐, 状态多了或者遇到需求的修改, 找到代码去修改就会很麻烦, 那么有没有一种不用写代码的方式, 只需要拖拖拽拽就能实现的方式呢?

使用状态控制器

状态控制器, 顾名思义是用于控制状态的, 状态其实可以理解为对应UI元素的属性值映射, 每一个状态映射一套UI元素的属性值, 切换状态则修对应UI元素的属性值, 即完成了状态切换.

效果展示

  • 支持控制器嵌套
  • 支持全属性/指定属性 保存

github: https://github.com/kirikayakazuto/CocosCreator_UIFrameWork/tree/master/packages/propcontroller

git 链接

1
https://github.com/kirikayakazuto/CocosCreator_UIFrameWork

0, 简单介绍

基于Cocos Creator的UI框架, 采用单场景+多预制体模式. 界面中的窗体都按需加载, 复用性高. 使用者只需要关心界面内的逻辑, 其他的
场景切换, 弹窗管理, 资源管理都可以交给框架.

对于游戏中的窗体, 可以大致分为5类.

  • Screen
  • Fixed
  • Window
  • Toast
  • Tips

以下图为例

Screen可以理解为场景, 一般是会铺满屏幕. 如上图黄色框中的地图.

Fixed则是一下固定在场景屏幕边缘的功能性UI, 如上图红色框中的两个按钮.

Window则是游戏中的各种弹出窗体, 一般会有一个弹出动画. 如上图蓝色框中的面板.

Toast 一个的窗体同时出现多个.

Tips则是一些提示性窗体, 比如断线提示窗体. 这种窗体的特点是不受其他窗体的影响, 只管自己显示和隐藏.

tips: Screen窗体切换时会隐藏当前显示的Fixed, Window窗体, 以到达切换场景的效果.

阅读全文 »

learn webgl