在Unity2D中创建角色描边的shader思路

我看到过很多角色描边的博客,大部分在讲3D的角色描边,在3D的情况下,最好的办法就是使用法线外扩+两次Pass来实现描边。简单来说就是因为模型有法线,那么顶点就会有法向,可以直接向外扩展部分,并额外渲染一个描边色的、不带正面的描边层,然后正常使用第二个Pass渲染模型本身就可以了。

而在2D中,没有模型,只有图片,并且图片也不存在硬边界,大部分的图片都是由一个简单的多边形加上扣掉无用的点组成的,这里给一个简单的例子来说明:

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
42
43
44
45
Shader "Hidden/test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Cull Off ZWrite Off ZTest Always
Tags { "Queue" = "Transparent" }
Pass//---------------------------渲染图片边框-----------------------------
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 白色
fixed4 col = fixed4(1,0,0,1);
return col;
}
ENDCG
}
}
}

选择一个附带透明像素的图片,将他赋予这个材质,你就会发现其实图片的边缘和我们看到的其实不一样。所以我们不能简单的通过控制顶点来实现我们的边缘效果。

强行控制会得到如下效果,可以看出并不能覆盖我们的源图片。


那我们该怎么做呢?

向像素边缘内收缩

这个方法的思路就是判断==非透明像素==的边缘像素,我们需要判断周围的像素中是否有透明像素即可。代码实现也是非常的简单,一个Pass就可以解决。直接上代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Shader "Custom/OutLineShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OutLineWidth("OutLineWidth",Range(0,5)) = 0.1
_OutLineColor("OutLineColor",Color)=(1,1,1,1)
}
SubShader
{
Tags{
"Queue" = "Geometry-20"
}
// 关闭剔除,让双面都能显示
Cull off
// 开启混合
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
// 关闭深度测试,使用Queue=Geometry-20的队列(用于2D游戏)
ZWrite off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag

struct inputV
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct outputV
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _OutLineWidth;
float3 _OutLineColor;
outputV vert(inputV v)
{
outputV o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag(outputV i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
float2 uv_up = i.uv + _MainTex_TexelSize.xy * float2(0,1) * _OutLineWidth;
float2 uv_down = i.uv + _MainTex_TexelSize.xy * float2(0,-1) * _OutLineWidth;
float2 uv_left = i.uv + _MainTex_TexelSize.xy * float2(-1,0) * _OutLineWidth;
float2 uv_right = i.uv + _MainTex_TexelSize.xy * float2(1,0) * _OutLineWidth;

float w = tex2D(_MainTex, uv_up).a * tex2D(_MainTex, uv_down).a * tex2D(_MainTex, uv_left).a * tex2D(_MainTex, uv_right).a;
// 描边
col.rgb = lerp(_OutLineColor.rgb,col.rgb,w);
return col;
}
ENDCG
}

}
}

是的,为了不出现不需要的开销,无论我们设置描边的大小为多少,我们都只采样材质五次,也就是说向周围的四个点采样,而不会逐像素采样(他们的区别是一个需要采样多次,而一个在描边宽度很大的时候会出现错误的结果。)
效果如下;



优点是快速、任何图片都可以使用,并且边缘比较均匀。缺点显而易见,它改变了图形原来的样子,向内侵占了部分像素。

图片偏移渲染边缘

既然不能控制顶点的移动,那么我们把图片简单的平移,正常渲染位置就好了。问题就是这种做法的成本还是有点太高了(((
代码我就只写一部分了,毕竟有四个高度重合的Pass:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
Shader "Hidden/test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BorderColor("BorderColor",Color)=(1,0,0,1)
_Offset("Offset",Range(0,1)) = 0.05
}
SubShader
{
Cull Off ZWrite Off
Tags { "Queue" = "Transparent" }
// 混合
Blend SrcAlpha OneMinusSrcAlpha
pass//----------------------渲染上边缘------------------------
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
fixed4 _BorderColor;
float _Offset;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.y += _Offset;
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _BorderColor*tex2D(_MainTex, i.uv).a;
return col;
}
ENDCG
}
pass//----------------------渲染右边缘------------------------
{
...
}
pass//---------------------渲染下边缘-------------------------
{
...
}
pass//---------------------渲染左边缘-------------------------
{
...
}
pass//------------------渲染模型本身----------------------------
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}


效果是很不错的,就是渲染需要整整四个Pass,实在不是太优雅,并且不能出现太大的描边,而且对很细的描边没办法很好处理:

判断边缘变透明像素为边界法

故名思意,判断==透明像素==是否为边缘像素,那么将该透明像素设置为边界颜色。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Shader "Hidden/test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BiggerBorder("BiggerBorder",Range(0,1)) = 0.5
_Offset("Offset",Range(0,1)) = 0.05
}
SubShader
{
Cull Off ZWrite Off
Tags { "Queue" = "Transparent" }
Pass//---------------------------渲染图片边框-----------------------------
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
float _Offset;
float _BiggerBorder;
v2f vert (appdata v)
{
v2f o;
// 顶点向外偏移,不是向上偏移
v.vertex.xy += normalize(v.vertex.xy) * _BiggerBorder;
o.vertex = UnityObjectToClipPos(v.vertex);
// 重采样uv
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if(col.a > 0.5)
{
return col;
}
else // 透明像素边界判断
{
fixed4 up = tex2D(_MainTex, i.uv + float2(0, _Offset));
fixed4 down = tex2D(_MainTex, i.uv - float2(0, _Offset));
fixed4 left = tex2D(_MainTex, i.uv - float2(_Offset, 0));
fixed4 right = tex2D(_MainTex, i.uv + float2(_Offset, 0));
if(up.a > 0.5 || down.a > 0.5 || left.a > 0.5 || right.a > 0.5)
{
return fixed4(1,0,0,1);
}
discard;
return fixed4(0,0,0,0);
}
}
ENDCG
}
}
}

效果与第一种方法不同,不会出现侵占纹理像素的情况,但是会被图片的边界所节点描边,导致描边不均匀。


不过这种方法也不失为一种好办法,因为可以在纹理图很细的时候实现比较好的效果,它不会修改任何的纹理上的像素。

Listary——不仅仅是搜索器

你是否受够了Windows的shit搜索?缓慢、不准确、无法模糊搜素……它除了能使用WIN键打开以外我找不到任何一个优点。

使用Listary搜索

现在Listary – Free File Search Tool & App Launcher摆在了你的眼前:只需要轻敲两次Ctrl键就能随时唤起你的搜索助手
Listary桌面查找示例
当然,就算我正在写这篇博客:我也能立刻查找到它
Listary软件使用中查找示例
不仅是精准的搜索,它有强大的模糊搜索能力,比如拼音

比如跨字

全屏屏蔽搜索设置

你可以设置在软件全局的时候屏蔽搜索

自定义命令

Listary给我提供了另一个无与伦比的便利——自定义命令,它可以将自定义热键绑定在你的自定义命令上!比如打开终端这件事情。在Ubuntu上,我从来都是使用Ctrl+Alt+T来打开终端的,但是在WIndows上,我已经受够了使用Win+R再加cmd来打开它了。这无疑是缓慢的。要设置自定义命令,你需要找到你的Listary程序,右键打开选项:

在这里,你可以非常方便的设置你的热键与对应的自定义命令:

快去尝试一下吧!使用独属于你的快捷方式!

我的编辑器

2024-12-15

今天来记录一下自己使用的编辑器——Obsidian - Sharpen your thinking,这是一款让我爱不释手的编辑器。
我的既然是和博客有关的编辑器,那肯定是指markdown编辑器了。我对编辑器有几点要求:

  • 可拓展
  • 可VIM
  • 可同步
  • 可源代码编辑
    而Obsidian完美的完成了所有的这些任务,并且给我带来了更多不一样的体验(原生和插件)包括:
  • 当前时间插入timestamper
    • 2024-12-15
    • 2024-12-15 11:05:02
  • 模板创建文章(原生插件)
    • 日记(主要用途)
    • TODO翻滚rollover daily todos(可以将前一天的TODO直接加入新的日记中)
  • 链接自动获取标题auto link title
  • 远程同步remotely save
  • VIM编辑(原生插件)
  • 自动生成hash
    • 0d7a719180ed

anzhiyu主题搭建记录

来自闲🐟老板芬达的订单

要求使用 github page 搭建安知鱼主题: 主题简介 | 安知鱼主题官方文档

先安装nodejs
Node.js — 在任何地方运行 JavaScript

1
2
3
4
5
6
7
8
9
10
11
# 安装hexo
npm install -g hexo-cli
mkdir dir
cd dir
hexo init
npm install hexo-deployer-git --save
git clone -b main https://github.com/anzhiyu-c/hexo-theme-anzhiyu.git themes/anzhiyu

npm install hexo-renderer-pug hexo-renderer-stylus --save
cp -rf ./themes/anzhiyu/_config.yml ./_config.anzhiyu.yml

修改配置文件主题到anzhiyu

创建github仓库,打开仓库page,创建机器ssh key。

修改配置文件,设置deploy:

1
2
3
4
deploy:  
type: git
repo: <repository url> # https://bitbucket.org/JohnSmith/johnsmith.bitbucket.io
branch: [branch]

推送hexo d

效果预览:Hexo
老板反馈:

使用HTTPS

前言

如果正常的话,我当前的域名应该是:https://hexo.zhywyt.me,为了庆祝我找到了免费的https方案,特地来写下这篇文章。

话不多说展示军火:

1
2
3
4
- frp
- nginx
- acme.sh
- ZeroSSL/Let's Encrypt

具体思路是,使用nginx做本地的两个服务,一个是81端口的http转https服务,一个是正常的80端口http服务。再使用acme.shZeroSSL或者Let's Encrypt自动申请证书,然后使用frp为本地 HTTP 服务启用 HTTPS | frp

然而你也可以不使用frphttps服务,那么你需要使用nginx进行认证,你可以参考:免费永久HTTPS(ssl)证书——Let’s Encrypt来了 - 大司徒 - 博客园

让我们开始HTTPS

frps

你需要启用frpshttps穿透,具体配置文件为:

1
2
3
4
5
6
7
8
9
bindPort = 7000
vhostHTTPPort = 80
vhostHTTPSPort = 443
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "****"
webServer.password = "****"
enablePrometheus = true
auth.token = "****"

重要的是你的vhostHTTPSPort字段,其他的请不要修改。修改完配置后重启frps,并打开你服务器的443端口。

nginx

为什么使用nginx因为我只会用这个,并且它的配置文件很有意思,且好玩。

使用frp

如果你使用了frp,那么你就不需要在nginx中设置443端口的https服务,只需要开放本机上的http服务即可。请修改下面的<yourservername>为你想要的名字。

1
2
cp /etc/nginx/sites-available/default /etc/nginx/sites-available/<yourservername>
ln -s /etc/nginx/sites-available/<yourservername> /etc/nginx/sites-enabled/<yourservername>

并将下面的配置文件写入你创建的文件中。请修改下面的<your server path>为你的网站路径,修改下面的<your domain>为你的域名。

1
2
3
4
5
6
7
8
9
10
server{
listen 81;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 80;
root <your server path> ;
index index.html;
server_name <your domain> ;
}

然后重启nginx

1
sudo systemctl restart nginx

acme.sh

我使用了acme.sh项目来实现证书的申请和自动更新!🥰
这是令人兴奋的,但是我有义务让你知道免费SSL证书的来源!我是从这篇知乎文章得知了这一渠道:为什么 SSL 证书要花钱购买,而不是政府免费发放的?怎么知道卖证书的是否可信呢?

接下来,让我们开始吧!

1
2
# 安装依赖
sudo apt update && sudo apt install unzip socat

然后你需要准备一个邮箱,并尝试运行下面的命令来下载acme.sh:

1
2
curl https://get.acme.sh | sh -s email=my@example.com
source ~/.bashrc

使用frp

接下来你需要准备:

  • 申请证书的域名
  • 服务的根目录
    并填入下面的命令,尝试执行它。
1
acme.sh --issue -d mydomain.com -d www.mydomain.com --webroot /home/wwwroot/mydomain.com/

然后你需要选择两个路径位置,用于存档certkey,你可以直接使用下面命令中的路径,否则你应该记住自己使用的路径,你应该修改下面的域名为你自己设置的域名

1
2
3
4
5
mkdir /etc/nginx/ssl
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/key.pem \
--fullchain-file /etc/nginx/ssl/cert.pem \
--reloadcmd "service nginx reload"

后续

到这里你可以使用下面的命令来查看是否成功安装证书了!

1
acme.sh --info -d example.com

并且你可以查看自己的定时任务:

1
crontab -e

上线网站

对于不使用frp的用户,其实重启nginx就可以正常工作了,但是对于frp用户,还需要进行一些frpc的配置。

打开你的frpc.toml,输入下面的配置:
你可以参考:为本地 HTTP 服务启用 HTTPS | frp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
serverAddr = "x.x.x.x"
serverPort = 7000

[[proxies]]
name = "test_htts2http"
type = "https"
customDomains = ["domain"]

[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:80"

# HTTPS 证书相关的配置
crtPath = "./server.crt"
keyPath = "./server.key"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"

并额外添加一个http的服务:

1
2
3
4
5
6
[[proxies]]
name = "httpname"
type = "http"
localIP = "localhost"
localPort = 81
customDomains = ["domain"]

最后重启frpc!大功告成!👋🛠️

如果有任何问题,欢迎向我提问!

实现自己的SHELL

前言

这篇文章会从头开始使用C语言编写一个可以交互的SHELL,并为其添加以下功能:

  • 运行可执行文件
  • 输入中断退出
  • cd/export/env/exit 内建命令
  • 实现了|管道通信
  • 实现了部分内部命令自动高亮ls/grep/cat
  • 记录历史命令
  • 检查运行环境

未实现功能:

  • & 后台任务 / fg / bg 恢复执行
  • && 依赖任务
  • || 多任务
  • / >> 重定向

任务要求

  • (1) 实现一个模拟的shell
  • (2) 实现一个管道通信程序

其实书上的写的任务里这些是独立的几个任务,但是我没注意看书上的要求,于是把(1)(2)都写在了SHELL中,也就是说我实现了一个可以使用

1
cat /path/to/file | grep symbel | grep symbel2

类似机制的指令的SHELL。这里记录、分析一下用到的一些系统调用/库函数。

前提

使用了readline库,并且使用了Linux管道调用,所以不能在Windows上复现。
安装readline库:

1
sudo apt update && sudo apt install libreadline-dev

创建简单的SHELL部分

对于shell,最重要最简单的功能就是能够读懂用户的指令了,比如最简单的:

1
2
3
4
5
6
7
8
9
ls
cat file
vim file
pwd
cd path
exit
export something=anotherthing
env
^D

又或者复杂一些的命令:

1
2
ls | grep pro
./exe && ./exe2 || ./exe3

我们的主函数的逻辑就像下面的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
char *input;
char *prompt;
char **args;
setup_signal_handlers();
rl_attempted_completion_function = shell_completion;
welcome();
while (1) {
prompt = get_prompt();
input = readline(prompt);
free(prompt);
if (!input) {
exit_shell();
}
if (strlen(input) > 0) {
add_history(input);
execute_command(input);
}
free(input);
}
return 0;
}

信号处理

从上往下看,首先第一个函数setup_signal_handlers,你需要在这里处理shell的信号机制,比如用户的中断信号Ctrl+C。普通的程序会使用正常的中断退出,但是作为一个shell,不应该在用户发出Ctrl+C中断信号的时候退出,而是应该处理Ctrl+D的输入终止信号才退出,所以我们可以得到一个简单的setup_ignal_handlers函数的定义:

1
2
3
4
#include <signal.h>
void setup_signal_handlers() {
signal(SIGINT, SIG_IGN);
}

这里的signal函数很重要,它的函数声明和使用示例为:

1
2
3
4
5
6
7
// reference
**void (*signal(int sig, void (*func)(int)))(int)**
// sample
signal(SIGINT, handleInterupt);// 自定义函数处理
signal(SIGINT, SIG_IGN);// 不处理
signal(SIGILL, SIG_DFL);// 默认处理
// ...

具体的细节可以参考菜鸟教程:C 库函数 – signal() | 菜鸟教程

我们只需要忽略掉SIGINT信号即可。

正则补全

然后是下面这行神奇的代码:

1
2
#include <readline/readline.h>
rl_attempted_completion_function = shell_completion;

右边是一个形如:

1
typedef char **rl_completion_func_t(const char * test, int start, int end)

的函数指针。该函数需要处理用户输入的不完整字符串的补全结果。readline库提供了一个正则匹配的函数:

1
2
3
4
extern char **rl_completion_matches PARAMS((const char *, rl_compentry_func_t *));
// sample
rl_compentry_func_t *generator = command_generator;
char* result = rl_completion_matches(text, generator);

上面的示例中,我们需要定义一个正则规则函数,形如:

1
char *command_generator(const char *text, int state);

的匹配函数,在这个函数中返回我们补全的结果。如果想要了解更多Readline中的补全机制,可以参考这篇博客。也可以直接读我的源码。
ReadLine自动补全分析 - LiuYanYGZ - 博客园
其实re_attempted_aompeltion_function类似于一个回调函数,会在用户按下Tab的时候尝试补全。

命令处理主体

再往后看,就是我们的函数主体了,在while循环中,我们需要处理用户的Enter输入,并执行对应的操作。并且需要为用户提供当前运行环境的一些信息,包括用户名、机器信息、路径信息等。这部分操作我们在get_prompt中实现,你可以随心所欲地实现你想要的提示词:

prompt

1
2
3
4
5
6
7
8
9
10
11
char *get_prompt() {
char cwd[1024];
char *prompt = malloc(1024);
struct passwd *pw = getpwuid(getuid());
if (getcwd(cwd, sizeof(cwd)) == NULL) {
strcpy(cwd, "?");
}
snprintf(prompt, 1024, "%s%s@%s%s:%s%s%s$ ", GREEN,
pw->pw_name, "myshell", RESET, BLUE, cwd, RESET);
return prompt;
}

这里调用了getpwuidgetuidgetcwd三个函数,你需要引入这些头文件:

1
2
#include <pwd.h>
#include <unistd.H.

add_history

add_historyreadline库提供的一个保存命令历史的函数,允许用户使用上下来查阅历史记录。因为上下箭头其实输入的也是一个字符串,可以判断并执行对应的操作。

execute_command

执行命令!到这里,用户已经输入了一个完整/不完整的命令,并按下了回车键,他希望你能帮他调用这些可执行文件,并得到对应的输出/或重定向到其他地方。首先允许我为你介绍一个分词函数:

1
char * strtok (char *str, const char * delimiters);

需要注意的是,这个函数会将第一参数分割,所以在处理它的时候需要进行复制处理。

下面是一个简单的循环取词的示例:

1
2
3
4
5
6
7
8
char * input = "I need a friend.";
char output[10][10];
char * word = strtok(input, " \t\n");
int i = 0;
while(word!=NULL){
output[i] = word;
word = strtok(NULL, " \t\n");
}

更多有关strtok的信息,请参考:(十六)strtok、strtok_s、strtok_r 字符串分割函数 - xtusir - 博客园

不适用docker的gitea runner配置

前言

最近在为我的gitea服务添加Actions模块,但是发现官网的方式有一个严重的问题:docker内无法正常访问外网,于是我就想使用自建容器来实现在本地运行的runner。下面是官网教程:
Overview | Gitea Documentation
趁着容器复制的时间来写了这篇记录文档。

安装act_runner

目标机器需要安装gitea/act_runner: A runner for Gitea based on act. - act_runner - Gitea: Git with a cup of tea,用于向自己的服务注册一个runner。下载自己对应的版本就可以了。然后需要创建一个配置文件,其中你的token可以在你自己的gitea管理界面下的action找到。如果找不到请查阅官方文档。

1
2
3
4
5
6
7
8
# 交互形式
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64
mv act_runner-0.2.11-linux-amd64 act_runner
mkdir runner && mv act_runner runner && cd runner
chmod +x act_runner
./act_runner register
./act_runner generate-config > config.yaml
vim config.yaml

要将它默认的几个容器配置删除,找到labels下的,修改为以下结构,其实就是去除了容器的形式,只允许使用host运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
*** config.yaml 2025-03-05 05:13:35.480359027 +0000
--- config.yaml.bk 2025-03-05 05:10:54.361732625 +0000
***************
*** 40,43 ****
labels:
! - "ubuntu:host"
!

--- 40,44 ----
labels:
! - "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
! - "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
! - "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"

然后运行测试一下是否能正常启动:

1
./act_runner daemon --config config.yaml

成功的输出大概是这样:

然后Ctrl+C终止它,回到工作目录,准备安装node。

安装node

来到官网,选择最新版,Linux和你最喜欢的下载器,这里我使用NVM测试。
Node.js — Download Node.js®
提前安装必须工具:

1
sudo apt install curl

下面的指令或许需要科学上网,请自行准备本机科学上网方式。
安装nvm

and install nvm:
1
2
3
4
5
6
7
curl -okh
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"

这里提供一个nvm换源的方法:nvm 切换国内镜像 | NVM
安装node

1
2
3
4
# 换源前
nvm install 22
# 换源后
nvm install 22.0.0

国内源很快,安装完后检查版本:

构建一个简单的测试

首先启动你的runner

1
./act_runner daemon --config config.yaml

然后在你的任意仓库建立一个文件:`.gitea/workflows/demo.yaml’写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]

jobs:
Explore-Gitea-Actions:
runs-on: ubuntu
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

这里最重要的是runs-on,需要配置为上面你在config.yaml文件里labels下定义的runner中选一个。Push后会自动开始构建,构建成功就像这样:

那么就大功告成了!然后就可以尝试其他的action了!

镜像actions/checkout仓库

在我的测试用,如果runner机器与github的联通不是很通畅的话,尝试从github拉取action的时候会出现错误,所以可以选择从自己的仓库中拉取,在上面的demo.yaml文件中,修改uses:字段为:

1
- uses: http://<您的gitea服务器地址>/actions/checkout@v4

即可访问,但是注意需要将镜像的仓库转让给actions组织,转让仓库可以在仓库设置的最下方找到:

给git加代理

不过我建议直接给runner加上git的代理,这样可以最方便地移植github上的action。

1
2
git config --global http.proxy <your proxy addr>
git config --global https.proxy <your proxy addr>

那这篇文章就到这里咯,谢谢您的阅读!

如何在Ubuntu上使用Cmake编译Glut

前言

2025-02-27 06:42:27
时隔一年,我再次重拾计算机图形学,内心怀揣着激动。面试官的一言,让我又燃起了对渲染的渴望。借着计算机图形学课程的机会,我想要重新认识一下这些经典的算法,并花一个学期的时间,编写一个软渲染的管线。我非常感谢字节的面试官。是他让我看到了工业界对渲染的需求,也让我再次燃起了大一刚学习图形学时的激情。

首先在开始前,明确一下我的测试环境。我使用的是Ubuntu22.04进行下面的流程测试。

依赖下载

首先我们安装一些必须的依赖,方便我们自己编译freeglut和opencv。

1
2
sudo apt update
sudo apt install cmake g++ gcc pkg-config build-essential unzip

然后我们可以前往freeglut官网opencv官网找到它们的发行版本,直接下载源码即可。测试使用的版本如下:

    • OpenCV – 4.11.0
  • freeglut - 3.6.0
1
2
3
4
5
wget https://github.com/opencv/opencv/archive/refs/tags/4.11.0.zip
wget https://github.com/freeglut/freeglut/releases/download/v3.6.0/freeglut-3.6.0.tar.gz
# 解压
unzip 4.11.0.zip
tar -xvf freeglut-3.6.0.tar.gz

编译glut和opencv

glut

1
2
3
4
5
cd freeglut-3.6.0
cmake -S . -B build
cd build
make -j56
sudo make install

这里记住安装的路径,一般为/usr/local/lib/

opencv

1
2
3
4
5
cd opencv-4.11.0
cmake -S . -B build
cd build
make -j56
sudo make install

配置pkg-config

编译并安装好动态/静态库后,我们要配置pkg-config的索引,前面的依赖安装中安装了pkg-config,你可以用这个测试它:

1
echo $PKG_CONFIG_PATH

有可能这个变量是空的,我们主要通过设置这个环境变量来添加pkg索引。你可以在任何你熟悉的地方编写环境变量的加载,这里我们使用profile.d/来添加。

1
2
touch /etc/profile/pkgconfig.sh
vim !$

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH加入该文件的最后一行。然后检查自己的库是否能被索引到:

1
2
pkg-config --libs opencv4
pkg-config --libs glut

类似的输出是正确的:

配置ld

然后我们需要为两个库编写ld conf。

1
2
sudo vim /etc/ld.so.conf.d/glut.conf
sudo vim /etc/ld.so.conf.d/opencv4.conf

最后重新载入

1
sudo ldconfig

两个文件都只需要写入/usr/local/lib即可。

CMakeLists.txt

这里使用了头歌上的两个简单的绘制代码来测试我们最后的环境:

student.h

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
// student.h
#include <GL/freeglut.h>
void myDisplay(void)
{

// 请在此添加代码,调用glClearColor函数
/********** Begin *********/


/********** End **********/
glClear(GL_COLOR_BUFFER_BIT);
// 请在此添加代码,实现其他渲染功能
/********** Begin *********/
//设置长方形颜色
//设置长方形位置
//将图形类型选定为三角形GL_TRIANGLES
//设置三角形顶点颜色和位置
//设置三角形顶点颜色和位置
//设置三角形顶点颜色和位置
//调用glEnd
//设置渲染点直径
//将图形类型选定为点GL_POINTS
//设置渲染点颜色和位置
//设置渲染点颜色和位置
//设置渲染点颜色和位置
//调用glEnd


/********** End **********/

glFlush();
}

test.cpp

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//test.cpp
// 提示:写完代码请保存之后再进行评测
#include <GL/freeglut.h>

#include<stdio.h>

// 评测代码所用头文件-开始
#include<opencv2/core/core.hpp>

#include<opencv2/highgui/highgui.hpp>

#include<opencv2/imgproc/imgproc.hpp>
// 评测代码所用头文件-结束
/*
(2).绘制一个矩形,颜色为(1.0f,1.0f,1.0f),矩阵位置(-0.5f,-0.5f,0.5f,0.5f);
(3).绘制一个三角形,三个顶点颜色分别为(1.0f, 0.0f, 0.0f), (0.0f,1.0f,0.0f), (0.0f,0.0f,1.0f),对应的顶点坐标分别为(0.0f,1.0f), (0.8f,-0.5f), (-0.8f,-0.5f);
(4).绘制三个直径为3的点,颜色为(1.0f, 0.0f, 0.0f), (0.0f,1.0f,0.0f), (0.0f,0.0f,1.0f),对应的顶点坐标分别为(-0.4f,-0.4f), (0.0f,0.0f),(0.4f,0.4f)。
*/
void myDisplay(void) {
// 请在此添加你的代码
/********** Begin ********/
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glPointSize(1);
glBegin(GL_QUADS);
glColor3f(1.0f, 1.0f, 1.0f);
glVertex2f(-0.5f,-0.5f);
glVertex2f(-0.5f,0.5f);
glVertex2f(0.5f,0.5f);
glVertex2f(0.5f,-0.5f);
glEnd();

glBegin(GL_TRIANGLES);
glColor3f(1.0f,0.0f,0.0f);
glVertex2f(0.0f,1.0f);

glColor3f(0.0f,1.0f,0.0f);
glVertex2f(0.8f,-0.5f);

glColor3f(0.0f, 0.0f, 1.0f);
glVertex2f(-0.8f,-0.5f);
glEnd();

glPointSize(3);
glBegin(GL_POINTS);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(-0.4f, -0.4f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex2f(0.4f, 0.4f);
glEnd();

/********** End **********/

glFlush();
}

int main(int argc, char * argv[]) {

glutInit( & argc, argv);
glutInitWindowPosition(100, 100);
glutInitWindowSize(400, 400);
glutCreateWindow("Hello Point!");
glutDisplayFunc( & myDisplay);
glutMainLoopEvent();

/*************以下为评测代码,与本次实验内容无关,请勿修改**************/
GLubyte * pPixelData = (GLubyte * ) malloc(400 * 400 * 3); //分配内存
GLint viewport[4] = {
0
};
glReadBuffer(GL_FRONT);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glGetIntegerv(GL_VIEWPORT, viewport);
glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_RGB, GL_UNSIGNED_BYTE, pPixelData);

cv::Mat img;
std::vector < cv::Mat > imgPlanes;
img.create(400, 400, CV_8UC3);
cv::split(img, imgPlanes);

for (int i = 0; i < 400; i++) {
unsigned char * plane0Ptr = imgPlanes[0].ptr < unsigned char > (i);
unsigned char * plane1Ptr = imgPlanes[1].ptr < unsigned char > (i);
unsigned char * plane2Ptr = imgPlanes[2].ptr < unsigned char > (i);
for (int j = 0; j < 400; j++) {
int k = 3 * (i * 400 + j);
plane2Ptr[j] = pPixelData[k];
plane1Ptr[j] = pPixelData[k + 1];
plane0Ptr[j] = pPixelData[k + 2];
}
}
cv::merge(imgPlanes, img);
cv::flip(img, img, 0);
cv::namedWindow("openglGrab");
cv::imshow("openglGrab", img);
//cv::waitKey();
cv::imwrite("../img_step2/test.jpg", img);
return 0;
}

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_minimum_required(VERSION 3.1)
project(StartGLUT)

find_package(OpenCV REQUIRED)
find_package(OpenGL REQUIRED)
find_package(GLUT REQUIRED)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)

include_directories($(OpenCV_INCLUDE_DIRS))
include_directories($(OpenGL_INCLUDE_DIRS))
include_directories($(GLUT_INCLUDE_DIRS))

add_executable(${PROJECT_NAME} test2.cpp student.h)

target_link_libraries(${PROJECT_NAME} ${OPENGL_LIBRARIES} ${GLUT_LIBRARIES} ${OpenCV_LIBRARIES})

编译

1
2
3
4
cmake -S . -B build
cd build
make
./StartGLUT
|