【Unity云消散】理论基础:实现SDF的8SSEDT算法

news2025/1/13 13:55:04

距离元旦假期已经过去5天了(从31号算起!),接着开始学习! 

游戏中的很多渲染效果都离不开SDF,那么SDF究竟是什么呢?到底是个怎么样的技术?为什么能解决那么多问题?

1 SDF

SDF,即signed distance field,有向距离场or带符号距离场(直译)。

1.1 记录了什么

SDF贴图,每个像素记录自己与距离自己最近的物体边界的距离。也就是说,SDF的贴图记录的竟然不是颜色信息,而是距离。 

图像的矢量表达

Signed Distance Field与Multi-channel signed distance field这篇文章更加直白的介绍了SDF——SDF本质上就是在一个光栅图里存了一个图像的矢量表达,说的太好了,真得就是这么一回事。

signed

所谓的signed体现在:像素点在物体内距离为负刚好在物体边界边界上为0。但其实这个物体内距离是正是负、其实只是一个判断的问题?(不太清楚这样说正确与否)

直接举几个例子吧。

1(直接截图了Signed Distance Field的内容)

2 直接了当的展示了signed(直接截图了SDF(signed distance field)基础理论和计算): 

3 这个表示方法更加形象理解“矢量”这个描述(直接截图了Signed Distance Field与Multi-channel signed distance field):

1.2 能干什么

Ray Marching

首先,Ray Marching,202有学习过:Lecture5 Real-time Environment Mapping

卡渲的面部光照

其次,风格化卡通渲染的人物面部光照,很多文章都有介绍,例如卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP)

SDF可以实现图像之间的平滑过渡,直接对两张SDF进行lerp插值就能得到视觉上的平滑过渡效果实际上是SDF贴图记录的距离之间的平滑过渡。 

其他的

除了RayMarching和面部光照,还有字体渲染(Unity中的TextMeshPro插件就是基于SDF的,还可以看看这篇文章)、做一些形变动画等等。

1.3 怎么做

8SSEDT算法(8-points Signed Sequential Euclidean Distance Transform),这是一种能在线性时间内计算出SDF的算法,基本上实现SDF都用的是8SSEDT吧。下面会着重学习一下这个算法是如何实现的。

2 8SSEDT算法

8ssedt其实是实现SDF的一种算法,还有其他方法,至于方法之间的对比以及简易程度这里我就略过啦!

2.1 算法核心:递推 

SDF的宗旨一直都是记录当前像素点离最近物体边界的距离。我们假设用0和1来表示当前像素点,0表示像素点值为空,1表示在物体,那么要找到距离当前点最近的目标像素点,就有以下两种情况:

  • 像素值为1:代表像素点自己就是目标点,距离为0
  • 像素值为0:代表当前像素点不是目标点,意味着向四周任意方向(上下左右/左上左下右上右下)都有可能是目标点

像素值为0时有以下两种情况: 

  • 上下左右的某个像素点的像素值为1
  • 左上左下右上右下的某个像素点的像素值为1

该像素点的SDF取值为:MinmumSDF(now.sdf, near.sdf + distance(now, near)),即附近像素点的SDF值+当前像素点到附近像素点的距离。

而公式里的near.sdf也同样需要用上述的式子计算,你会发现SDF图像记录的距离都是连续的,所以SDF算法的内核其实是递推,把复杂的问题拆解成了连续的简单问题。

伪代码

now.sdf = 999999;// 初始为空,距离尽可能大
if(now in object){// 
    now.sdf = 0;// sdf距离为0
}else{// 像素点值为0
    foreach(near in nearPixel(now)){
        now.sdf = min(now.sdf,near.sdf + distance(now,near));// 递推
    }
}

2.2 算法核心思路

STEP1 加载图片并创建2个Grid

假设我们拿到一张黑白图,加载图片后,遍历一遍黑白图,此时的原图里假设是白色为物体,黑色为空,建立两个Grid来记录网格像素数据,用以确定像素点是位于物体内部还是外部,如何建立如下:

  • 一个Grid用于计算物体外到物体的距离:那么从距离的角度来讲,我们可以标记白色像素距离为0,黑色像素距离为一个尽可能大的数(例如上面的now.sdf = 999999),这个Grid用于推导向外的距离场
  • 一个Grid用于计算物体内部点到物体外的距离:即黑色像素标记距离为0,白色像素标记为一个尽可能大的值,这个Grid用于推导向内的距离场

也就是一个Grid缓存内部为0,外部为无穷;一个Grid缓存内部为无穷,外部为0。

STEP2 计算距离

接下来干什么呢?让一个像素与周围的8个像素分别进行比较

按照“从左往右,从上到下”(如果是引擎UV生成还要考虑方向,比如Unity的UV是从下往上)的顺序遍历所有的像素。

文章Signed Distance Field - 知乎 (zhihu.com)中把过程拆解成了两个PASS,每个Grid都经历一次PASS0和PASS1,具体过程可以直接戳这篇文章看就行!过程包括伪代码都写的很详细~

STEP3 两个Grid作差

grid1(pixel).sdf - grid2(pixel).sdf就行。

2.3 Excel直观理解算法

这里我简单实践一下帮助更好的理解,选择了一个笨蛋办法,在excel里进行:

STEP1 计算Grid1

第一个Grid原始值如下(用9直接替代∞):

PASS0后:

PASS1后最终:

STEP2 计算Grid2

第二个Grid原始值如下(用9直接替代∞):

PASS0和PASS1后最终:

STEP3 相减

对每个对应位置像素Grid1.sdf - Grid.sdf后,才算得到最终的signed结果:

这样得到的就是最后的SDF贴图每个像素点储存的距离了,当然这只是个简单的例子,得到真实的应该是类似于下图的样子(这张截图来自这篇文章,上面也有类似的例子展示):

3 代码实现

学习的过程中我发现绝大部分文章的代码都来自于这篇文章:Signed Distance Fields (codersnotes.com),Signed Distance Field中对过程做了解释,但是呢如果直接运行你会发现缺少一个SDL库。

3.1 SDL库

上述代码运行还需要添加SDL库,具体如何添加以及SDL库是什么可以直接看这篇文章:

SDL库的介绍与安装

捣鼓捣鼓你会发现,上述代码其实用的是SDL1.2版本(毕竟是2006年的文章了),有一些函数已经被替换/删除了,且64位的电脑上支持不了一些头文件,例如SDL_config.h就是SDL_config_win32.h

所以我们不仅要添入SDL库,还需要对比1.2和2.0的不同修改代码,修改的过程就不赘述了,后面的完整代码里我会写一些注释,这里简单列举一下我参考的一些文章:

SDL2常用函数&结构分析:SDL_Surface&SDL_GetWindowSurface&SDL_LoadBMP

SDL学习笔记(3)——窗口绘制 

3.2 最后的完整代码

部分函数的使用我写了一些注释方便理解,修改后的最终代码如下:


#include <SDL.h>
#include <SDL_main.h>
#include <math.h>

#pragma comment(lib, "SDL2.lib")
#pragma comment(lib, "SDL2main.lib")

#define SDL_MAIN_HANDLED

#define WIDTH  256
#define HEIGHT 256

struct Point
{
	//dx,dy表示对于当前点的偏移值
	int dx, dy;

	int DistSq() const { return dx * dx + dy * dy; }
};

struct Grid
{
	Point grid[HEIGHT][WIDTH];
};

Point inside = { 0, 0 };
Point empty = { 9999, 9999 };
Grid grid1, grid2;

Point Get(Grid& g, int x, int y)
{
	// OPTIMIZATION: you can skip the edge check code if you make your grid 
	// have a 1-pixel gutter.
	if (x >= 0 && y >= 0 && x < WIDTH && y < HEIGHT)
		return g.grid[y][x];
	else
		return empty;
}

void Put(Grid& g, int x, int y, const Point& p)
{
	g.grid[y][x] = p;
}

void Compare(Grid& g, Point& p, int x, int y, int offsetx, int offsety)
{
	//获取当前点偏移后的点
	Point other = Get(g, x + offsetx, y + offsety);
	//给获取的点的dx和dy设置对应的偏移值
	other.dx += offsetx;
	other.dy += offsety;

	if (other.DistSq() < p.DistSq())
		p = other;
}

// Now all we have to do is run the propagation algorithm. See the paper for exactly what's happening here, 
// but basically the idea is to see what the neighboring pixel has for it's dx/dy,
//  then try adding it onto ours to see if it's better than what we already have.
void GenerateSDF(Grid& g)
{
	// Pass 0
	//遍历当前点以及左右、左下、右下、正下方的点,找到距离最短的点存储在网格对应位置处
	for (int y = 0; y < HEIGHT; y++)
	{
		for (int x = 0; x < WIDTH; x++)
		{
			Point p = Get(g, x, y);
			Compare(g, p, x, y, -1, 0);
			Compare(g, p, x, y, 0, -1);
			Compare(g, p, x, y, -1, -1);
			Compare(g, p, x, y, 1, -1);
			Put(g, x, y, p);
		}

		for (int x = WIDTH - 1; x >= 0; x--)
		{
			Point p = Get(g, x, y);
			Compare(g, p, x, y, 1, 0);
			Put(g, x, y, p);
		}
	}

	// Pass 1
	for (int y = HEIGHT - 1; y >= 0; y--)
	{
		for (int x = WIDTH - 1; x >= 0; x--)
		{
			Point p = Get(g, x, y);
			Compare(g, p, x, y, 1, 0);
			Compare(g, p, x, y, 0, 1);
			Compare(g, p, x, y, -1, 1);
			Compare(g, p, x, y, 1, 1);
			Put(g, x, y, p);
		}

		for (int x = 0; x < WIDTH; x++)
		{
			Point p = Get(g, x, y);
			Compare(g, p, x, y, -1, 0);
			Put(g, x, y, p);
		}
	}
}

int main(int argc, char* args[])
{
    // SDL_Init(SDL_INIT_VIDEO) -- 初始化视频子系统
	if (SDL_Init(SDL_INIT_VIDEO) == -1)
		return 1;

	// 创建一个窗体
	SDL_Window* window = SDL_CreateWindow("W", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, 0);
	// 创建一个与窗体关联的surface,赋值给screen
	SDL_Surface* screen = SDL_GetWindowSurface(window);
	if (!screen)
		return 1;

	// 加载位图文件
	// SDL_LoadBMP -- 加载指定的bmp文件,一定要是bmp图像的文件
	SDL_Surface* temp = SDL_LoadBMP("test.bmp");

	// 调整窗口
	// SDL_ConvertSurface -- 图片被加载时对其以屏幕相同格式进行转化来保证投射过程中不再发生转化,以新的格式返回原来的surface
	temp = SDL_ConvertSurface(temp, screen->format, SDL_SWSURFACE);

	SDL_LockSurface(temp);
	for (int y = 0; y < HEIGHT; y++)
	{
		for (int x = 0; x < WIDTH; x++)
		{
			Uint8 r, g, b;
			Uint32* src = ((Uint32*)((Uint8*)temp->pixels + y * temp->pitch)) + x;
			SDL_GetRGB(*src, temp->format, &r, &g, &b);
			// Points inside get marked with a dx/dy of zero.
			// Points outside get marked with an infinitely large distance.
			// 两个网格,一个内部设置成0,外部设置成正无穷,另一个网格相反。
			if (g < 128)
			{
				Put(grid1, x, y, inside);
				Put(grid2, x, y, empty);
			}
			else {
				Put(grid2, x, y, inside);
				Put(grid1, x, y, empty);
			}
		}
	}
	SDL_UnlockSurface(temp);

	// Generate the SDF.
	GenerateSDF(grid1);
	GenerateSDF(grid2);

	// Render out the results.
	SDL_LockSurface(screen);
	for (int y = 0; y < HEIGHT; y++)
	{
		for (int x = 0; x < WIDTH; x++)
		{
			// Calculate the actual distance from the dx/dy
			//计算偏移值的点距离当前点的距离
			int dist1 = (int)(sqrt((double)Get(grid1, x, y).DistSq()));
			int dist2 = (int)(sqrt((double)Get(grid2, x, y).DistSq()));
			int dist = dist1 - dist2;

			// Clamp and scale it, just for display purposes.
			int c = dist * 3 + 128;
			if (c < 0) c = 0;
			if (c > 255) c = 255;

			Uint32* dest = ((Uint32*)((Uint8*)screen->pixels + y * screen->pitch)) + x;
			*dest = SDL_MapRGB(screen->format, c, c, c);
		}
	}
	SDL_UnlockSurface(screen);

	// Wait for a keypress
	SDL_Event event;
	while (true)
	{
		if (SDL_PollEvent(&event))
			switch (event.type)
			{
			case SDL_QUIT:
			case SDL_KEYDOWN:
				return true;
			}
		// 更新窗口,才能看到
		SDL_UpdateWindowSurface(window);
		// 保存成位图
		SDL_SaveBMP(screen, "save2.bmp");
	}
	
	
	return 0;
}

3.3 运行展示

首先是一个基础的:

PS随便画了一张(需要保存成bmp格式):

4 面部阴影(挖个坑)

SDF在风格化渲染中用的最多的地方其实是实现面部阴影,通常是美术绘制好特定光线角度时的面部阴影,通过SDF插值计算出中间的过程,将过程叠加到一张图上,通过简单的blur或者smooth操作实现平滑。

目前已经有大佬给出快速生成上述图的程序了:如何快速生成混合卡通光照图 - 知乎 (zhihu.com)

这里就简单的提一下吧,也是挖个坑(要学的好多!),等实现了天空盒之后就来继续学习面部阴影!! 

参考

(其实参考了非常多的文章,但是后面写着写着忘记了都有哪些了,这里简单的罗列三个吧~)

【有趣的技术】Unity中的SDF(有向距离场) - 简书 (jianshu.com)

Signed Distance Field - 知乎 (zhihu.com)

Tech-Artist 学习笔记:Signed Distance Field 8SSEDT 算法 - 知乎 (zhihu.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/145647.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git介绍及环境搭建

git介绍及环境搭建Git介绍Git安装流程配置用户信息git工作流程与常用命令问题点总结主要工作流程git工作流程与原理总结Git介绍 1.Git是什么&#xff1f; Git版本控制系统是一个分布式的系统,是用来保存工程源代码历史状态(游戏存档)的命令行工具 GIT是一个命令行工具,用于版…

基于Java+Spring+vue+element社区疫情服务平台设计和实现

基于JavaSpringvueelement社区疫情服务平台设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源…

Django+channels -> websocket

Django+channels -> websocket 学习视频: https://www.bilibili.com/video/BV1J44y1p7NX/?p=10 workon # 查看虚拟环境 mkvirtualenv web -p python3.10 # 创建虚拟环境 workon web # 进入虚拟环境pip insatll django channelsdjango-admin startproject ws_demo python …

【NI Multisim 14.0原理图环境设置——元器件库管理】

目录 序言 一、元器件库管理 &#x1f349;1.“元器件”工具栏 &#x1f34a;&#xff08;1&#xff09;电源/信号源库 &#x1f34a;&#xff08;2&#xff09;基本器件库 &#x1f34a;&#xff08;3&#xff09;二极管库 &#x1f34a;&#xff08;4&#xff09;晶体管…

seL4 背景知识

1 seL4 演变 1.1 微内核 微内核发展到目前为止经历了三代, 这里做一些归纳。参考《现代操作系统: 原理与实现》中操作系统结构一章, 关于微内核架构发展的介绍。 第一代微内核设计将许多内核态功能放到用户态, Mach 微内核是第一代微内核的代表。第二代微内核设计将对 IPC 优…

C++学习记录——일 C++入门(1)

C入门&#xff08;1&#xff09; 文章目录C入门&#xff08;1&#xff09;一、C关键字二、C第一个程序三、命名空间1、域作用限定符2、了解命名空间3、命名空间的使用四、C输入输出五、缺省参数六、函数重载七、引用1、引用符号2、引用的部分使用场景一、C关键字 关键字有98个&…

filebeat采集nginx日志

背景我们公司项目组用的是elastic的一整套技术栈&#xff0c;es&#xff0c;kibana&#xff0c;filebeat和apm&#xff0c;目前已经可以采集网关各个微服务的日志。架构图现在需要在原来的基础上把nginx这的日志也采集上来&#xff0c;方便做链路跟踪问题与思路原先traceId是在…

数字经济时代,“8K+”开拓行业新格局

2023深圳国际8K超高清视频产业发展大会召开&#xff0c;大会以“超清互联 数智创新”为主题&#xff0c;汇聚两院院士、产业领袖、领军企业共同深入探讨超高清产业发展现状、关键问题和未来趋势&#xff0c;并集中发布《深圳市超高清视频显示产业白皮书&#xff08;2023版&…

「数据密集型系统搭建」开卷篇|什么是数据密集型系统

在我们开发的诸多系统&#xff0c;基本都可以视为“数据密集型系统”&#xff0c;数据是一切物质的载体&#xff0c;我们依靠数据做存储记录&#xff0c;通过数据进行信息传递交换&#xff0c;最终还要数据来呈现和展示等&#xff0c;从一定视角而言&#xff0c;系统中最核心、…

临时用网搞不定?别着急,5G网络“急救车”来啦

如何在1天时间内&#xff0c;用不超过5名装维人员&#xff0c;完成超过200间宿舍的网络覆盖&#xff0c;让即将踏上考场的高三学子们尽快用上网络&#xff1f; 近期&#xff0c;这个问题一直困扰着重庆电信客户经理周睿。原来&#xff0c;由于疫情原因&#xff0c;重庆市某中学…

WINDOWS安装Oracle11.2.0.4

(一)Oracle服务器端安装 1.运行Oracle11g服务器端安装程序setup.exe,弹出如下界面&#xff1a; 2.如上界面中&#xff0c;把默认打上的勾去掉&#xff0c;然后点击【下一步】&#xff0c;弹出如下界面&#xff1a; 3.如上界面中&#xff0c;选择跳过软件更新,然后点击【下一步…

指针进阶(三)再谈数组与串函数

&#x1f31e;欢迎来到C语言的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f320;本阶段属于练气阶段&#xff0c;希望各位仙友顺利完成…

【阶段二】Python数据分析数据可视化工具使用01篇:数据可视化工具介绍、数据可视化工具安装、折线图与柱形图

本篇的思维导图: 数据可视化工具介绍 Matplotlib是最著名的绘图库,主要用于二维绘图,当然也可以进行简单的三维绘图。它提供了一整套丰富的命令,让我们可以非常快捷地用Python可视化数据,而且允许输出达到出版质量的多种图像格式。 Seaborn是在matplo…

国内电容市场份额达七成,松下如何抢占高地?

01 电容市场发展 电容器是三大电子被动元器件之一&#xff0c;是电子线路中不可缺少的基础元件&#xff0c;约占全部电子元件用量的40%&#xff0c;产值的66%。中国电容器行业规模增速持续高于全球规模增速&#xff0c;中国市场的快速增长成为拉动全球电容器行业规模增长的主要…

【Python从入门到进阶】2、Python环境的安装

接上篇《1、初识Python》 上一篇我们对Python这门编程语言进行了一个基本的了解&#xff0c;本篇我们来学习如何下载安装Python编程环境&#xff0c;以及如何使用pip管理Python包。 本篇讲解的是Windows环境下安装Python编程环境的步骤。 一、Python安装包下载 想要使用Pyth…

vue框架、element-ui组件库、font awesome图表库

一、vue 创建一个新vue项目。 vue create ProjectName 然后cd到该目录下&#xff0c;npm run serve启动服务器&#xff0c;即可打开。 二、组件库 element-ui是饿了么的&#xff0c;ArcoDesign是字节的&#xff0c;有很多。 install见官方文档&#xff1a;组件 | Element 导入…

黑马学SpringAMQP

目录&#xff1a; &#xff08;1&#xff09;SpringAMQP的基本介绍 &#xff08;2&#xff09;SpringAMQP-入门案例的消息发送 &#xff08;3&#xff09; SpringAMQP-入门案例的消息接收 &#xff08;4&#xff09;SpringAMQP-WorkQueue模型 &#xff08;5&#xff09;Sp…

408数据结构考点总结

第一章 绪论 考点 1&#xff1a;时间复杂度与空间复杂度 时间复杂度 定义&#xff1a;将算法中基本运算的执行次数的数量级作为时间复杂度&#xff0c;记为O(n)O(n)O(n)。 计算原则 加法法则&#xff1a;T(n)T1(n)T2(n)O(f(n))O(g(n))O(max⁡(f(n),g(n)))T(n)T_{1}(n)T_{2…

安全—02day

XMLHttpRequest 对象 AJAX 通过原生的XMLHttpRequest对象发出 HTTP 请求&#xff0c;得到服务器返回的数据后&#xff0c;再进行处理。现在&#xff0c;服务器返回的都是 JSON 格式的数据&#xff0c; XMLHttpRequest本身是一个构造函数&#xff0c;可以使用new命令生成实例。…

T113_PRO-S3_GPS解析

今天主要是以市面上能买到的最新款的GPS作为解析的依据&#xff0c;市面上能买的GPS目前最新的串口打印的格式主要是如下&#xff1a; $GNGGA,085126.000,2311.77819,N,11323.44968,E,1,15,1.5,36.7,M,0.0,M,,*4C $GNGLL,2311.77819,N,11323.44968,E,085126.000,A,A*4B $GPGSA…