EZ² - TANotes
type: channel
link: http://www.erinz.xyz/index.php/category/Notes
items: 💡学点儿丨Ra计划(1)-原画-SD风格模型训练&模型融合
items: 🕹️OpenGL丨Hi IRA - 皮肤渲染(6)
items: 🕹️OpenGL丨实时皮肤渲染效果优化 - 皮肤渲染(5)
items: 🕹️OpenGL丨魔方最终版！ - 魔方作业(5)
items: 🕹️OpenGL丨半边结构模型读取&显示[2]
items: 🕹️OpenGL丨半边结构模型读取&显示[1]
items: ODOS丨好久不见，水个阿基米德螺线
items: 🕹️OpenGL丨可分离的次表面散射SSSS - 皮肤渲染(4)
items: 🕹️OpenGL丨ShadowDepthMap实现SSS透射 - 皮肤渲染(3)
items: 🕹️OpenGL丨延迟渲染&帧缓冲 - 皮肤渲染(2)

💡学点儿丨Ra计划(1)-原画-SD风格模型训练&模型融合
type: item
date: 2023-11-07T06:39:00+08:00
description: 什么是Ra计划 一张角色美术工作流的体验卡，主打一个学学再说。大概会捋一下AI原画-雕刻-模型-材质-绑定-动画的全流程，是个大坑。 Part1 原画 这一趴其实是来凑数的！Ra的原画是直接Midjourney抽出来的，其精度也达不到[原画]的要求，后期一边建模一边创作。 本文是张冠李戴的另一个MJ画风模型训练，但是基本是一样的原理，我是不介意多水一篇啦。 StableDiffusionv1.5 在实习期间接触了很多AI相关的尝试，关于AI原画的知识也来源于工作的积累。 11月了才来发这篇文章属实是大迟特迟，但还是想把之前学习的一些东西记录一下。所以先行预警，这是一篇（至少）过时了4个月且七拼八凑的训练笔记，不是教程！ 不是教程！ 请希望学习模型训练的读者查找最新SDXL的相关教程。 训练原理 模型微调与DreamBooth 模型微调 Stable Diffusion 的图像生成主要涉及三个主要模块：CLIP（Text Encoder），U-Net 和 VAE。 CLIP：将 Prompt 转化成模型能够理解的向量 U-Net:图像生成的主要网络，由于结构与字母 U 相似而得名 VAE：将噪声转化成输出图像 模型微调的过程就是用少量的图像对 Text Encoder 和 U-Net 两部分做微调，使模型学会我们新加入的风格或概念。但是由于微调过程中素材量较低，可能会发生过度拟合（比如训练集中只有趴着的小狗，就无法生成站着 or 跑步的小狗）和语言偏移（随微调逐渐失去语言的句法和语义知识）。 DreamBooth 为了避免这两个问题，DreamBooth 的训练方法提出了 Prior-Preservation Loss 先验损失，通过自己先生成一些图像（也是我教程里的 class image），来保留先验损失权重。用模型自己生成的样本来监督模型，以便在 few-shot（小样本，可以理解为几张图） 微调开始后保留先验知识。 举例中，我们训练特定长相的小狗，只有四张图片作为训练集。我们称它为 sks dog，其中 sks 是一个特殊的 token 关键词，dog 为狗狗的大类 class。dreambooth 会根据 dog 这个 class 生成大量其他小狗的图像，在学习新概念 sks 的特征时保留对狗狗大类的认知。 在现在使用的各种脚本中，先验损失保留这个过程也被称为“正则化”。另外允许训练时使用自己准备的训练集，即“reg dataset”。如果需要使用正则化，正则训练集的图像就准备同类别图像即可（例如训练角色 - 同性别的单人图）。 LoRA 通过 Dreambooth 和传统的微调方法，我们会得到“大模型”一般在 4-7GB 左右，且训练的过程占用显存较高，用时也比较久。LoRA 是在 Dreambooth/ 基础微调方法的基础上产生的。 LoRA ,英文全称Low-Rank Adaptation of Large Language Models ,直译为大语言模型的低阶适应,这是微软的研究人员为了解决大语言模型微调而开发的一项技术。LoRA建议冻结预训练模型的权重并在每个Transformer块中注入可训练层(秩-分解矩阵)。因为不需要为大多数模型权重计算梯度,所以大大减少了需要训练参数的数量并且降低了GPU的内存要求。 简单来说, LoRA只训练模型中的一部分参数。训练比例由秩dim(rank)决定,dim越大,显存要求越高,生成的模型文件也越大,也可以学到更多图像特征。一般dim128的LoRA训练会生成144MB左右的LoRA模型,生成时需要搭配大模型使用。 dim 并非越大越好，通常 128 已经能够满足绝大多数训练需求。人物训练甚至只需要 32-64 即可。 U-Net与层 UNet 是图像生成的主要结构，整体形态呈 U 形。上图是 Stablediffusion 中使用的 Unet 结构。图像从左上角输入，由左向右传递，最终从右上角输出。 左侧下行会经历三次降采样，共包含 IN0-IN11 共 12 层。中间是 MID 层。右侧上行会经历三次升采样（IN3、IN6、IN9），共包含 OUT0-OUT12 共 12 层。横向粉色的连线表明特征信息的传递，IN 与 OUT 的部分层会相互关联影响。 LoRA 通过 Attention 模块干涉所有图中橙色的层。而 Lycoris 的一些结构可以干涉所有 Res 层，即深蓝色的层。 层的差别 - 上&下 在实际效果上，偏上的层决定画面上更细微的特征，例如材质、边缘等。偏下的层决定宏观的特征，例如构图、色彩等。这主要是受到降采样的影响： 在 IN0 中，图像首先被缩放到原图的 1/8，然后再 IN3/6/9 时再分别缩小 1/2。 Stablediffusion 使…

🕹️OpenGL丨Hi IRA - 皮肤渲染(6)
type: item
date: 2023-03-05T20:55:00+08:00
description: 上学期收到作业附件时，关于这个脑袋与SSSS的资源只有区区6页的一个slide，obj与diffuse，normal，specular三张贴图。 之前动视暴雪中皮肤的部分已经基本实现完了，但眼睛的渲染优化 GDC2013 Next Generation Character Rendering 溯源从之前暴雪GDC渲染的Animation部分开始，讲座介绍模型及表情的数据来自Graphics Lab of the Institute of Creative Technologies of USC，使用Lightstage X扫描。 在ICT USC的网站中详细介绍了近年来的工作，在2014年其发表了与视频驱动的表情捕捉相关的论文，其题图正是我们要找的这位仁兄！ 文末的相关项目为我们指明了前路：Digital Ira，发布于2013年，是专为实时渲染而扫描制作的人脸数据集。 （顺便吐槽一下ICT网站，大概是前些年换过域名，所有的站内超链接通通失效,,得单独去谷歌题目才行) Digital Emily 但Ira其实并非ICT扫描的第一个数字人，在2008年，ICT扫描了Emily O'Brien，并开展了Digital Emily项目。 扫描使用了15张不同光照的照片以计算面部的几何形状和反射率。通过使用不同的光源角度和偏振器，我们能够得到diffuse+specular的图像以及滤除高光的纯diffuse图像，从而观察面部的高光反射。不过这里的'specular'图像不是完全黑白的，这是因为部分光线反射前已经经历过一次散射，改变了色光的比例。 高光图是法线计算的重要依据，通过视线向量和光线向量可以反推出面部法线。 刚刚提到不同色光的分布在高光中分布并不一致，不同色光分量得到的法线图也是不一样的：红光散射的最远，因此法线细节会柔和许多。 好消息是，Digital Emily的模型、贴图是开源的，在Wikihuman Project下可以获取Emily的完整Maya场景与Shader文件。 （题外话——写完才想起来，这个项目之前动画原理课讲人脸动画时就讲过哇！） Hi Ira Ari Shapiro是该实验室的图形研究员，Digital Ira项目旨在创建一个实时的、照片真实的数字人物，数据集也分享给了Nvidia和动视暴雪，以进行技术演示（That's why we found him!) 。 Digital Ira与GDC2014的Digital ira and beyond可以获取更多关于Ira模型生成、表情系统构建的信息，但ICT并没有对Ira的模型及纹理信息进行公开:(((( 不过这篇文章详细介绍了Nvidia的Ira和动视的Ira，荐读一下👈 于是我们只好把目标转向拿到了数据的Nvidia和动视暴雪。Nvidia于2013年发布了这个面部渲染的Demo，可以在这里下载。 悲报是，作为一个成熟的demo，它的所有资源打包成了apk.......而我并不会解包...... Ira会面之旅就此卡住。我试了好多不同的关键词组合都没有找到新的内容——直到把Ira踢出了搜索框，转而搜索上述demo的项目名FaceWorks，终于找到了这个demo的Github开源版本😭😭😭😭 其模型等资源位于Faceworks/samples/media/DigitalIra.贴图的命名与作业附件的材料都是一致的，说不定之前助教也是从这里存下来的嘞💫 很高兴为Ira找回了眼睛和一丢丢毛发（以及maya的反光shader）！Nvidia的demo版本中并没有使用动视暴雪提到的眼周高光与虹膜渲染的优化，所以没有提供Jorge等人使用的泪光闪闪mesh&睫毛遮罩贴图等。下一篇我们就尝试复现一下暴雪眼部渲染的优化流程！期待！

🕹️OpenGL丨实时皮肤渲染效果优化 - 皮肤渲染(5)
type: item
date: 2022-12-09T22:59:00+08:00
description: 前情提要： 🕹️OpenGL丨直接光照PBR - 皮肤渲染(1) 🕹️OpenGL丨延迟渲染&帧缓冲 - 皮肤渲染(2) 🕹️OpenGL丨ShadowDepthMap实现SSS透射 - 皮肤渲染(3) 🕹️OpenGL丨可分离的次表面散射SSSS - 皮肤渲染(4) 本文主要内容为GDC2013 Next-Generation Character Rendering 的ppt笔记整理总结。如果有空的话还会再研究一下UE5中的具体实现。 令人惊叹竟然已经马上十年过去了..... 第一部分我们主要关注皮肤渲染的细节实现。 皮肤反射 - 双镜叶高光与压表面结构 Gramham(2013)论文通过测量提出皮肤的高光具有两个独立的高光波瓣，Jorge等通过使用不同的粗糙度计算两个高光项来模拟这样的视觉效果，然后以85%,15%的方式线性插值两个高光。 分享中建议第二个高光波瓣的gloss值可以使用2倍的原gloss值，或将specular的指数乘方。 在作业项目中，以0.85插值得到的双镜叶高光效果如下： 这样我们其实已经得到了不错的表面压结构，但皮肤的微小褶皱会打破高光的形状，Jorge等使用了一张类似噪波的法线纹理来实现这个效果： 距离稍微离远一点的效果是这样的：成片的毛孔结构变成了更细碎的高光。 这里使用到的噪波纹理是由多个正弦波叠合而成的。 注：这里的rand()并非每像素随机，而应当是一个提前确定的随机数表。 在作业项目中，使用SD制作了这张噪波纹理。理论上其实应该用FX-map来做循环，但一直传循环数失败（...)，所以改用pixel processor写了一个正弦波生成器，再用Tile Generator去实现随机位置和随机大小来模仿上述的公式。 ——效果肯定是不如原文的，一方面pixel processor生成的是一定区域内的正弦波，虽然给了比较大的缩放值，但必然有些部分没有完全覆盖；另一方面缩放损失了一定图像质量。最终生成的噪波纹理只剩下了颗粒感，没有像原文中那么多细线条的纹理效果。 如果用FX-map理论上是可以的—— 先用Quadrant将纹理分到1024*1024(因为FX-map不是逐像素的而是逐细分(?)的，$Pos是细分的中心位置)，然后用iteration处理循环，并将当前循环次数传给计算的Quandant用作于Global Random的seed，剩余绘制跟下面贴的pixel processor差不多~ 获得噪波纹理后，我们要将这些细节与原法线混合，Jorge等推荐使用的混合方法是：Reoriented Normal Mapping (RNM)，详细解释可以参考这里。 （但但但....我用RNM直接套公式算出来不太对.......） （太困了，以后有机会再改，这里直接用的线性混合） 这些表面的亚结构也会作用于高光上，计算方法如下： 为了增加美术对该模型的控制，可以对base glossiness和细节变化结合得到： \frac{1}{s'}=\frac{1}{s}+\delta ^2 注意这里的base glossiness应当已经包含了normal map中的表面变换。 总结一下！在本工作流中，gloss map应该只包含低频度的细节信息，比如T区的划分等。 中频度的细节信息储存于normal map中，高频度的细节信息存储于detail tiled noise normal map中。 这两种normal的表面变化都会反应到粗糙度的最终结果上。 （but这个我作业项目里没写） 关于数值的具体信息，Jorge给出了一个参考表格，但还是需要美术根据具体的灯光环境进行微调。 Second Lobe对应的数值应为 2.0*G 或者 S^2.0。 皮肤毛孔 人类皮肤的毛孔是一个非常粗糙复杂的孔状结构，当它被简化至模型与法线表示时，它就变成了一个“盆地”，which means毛孔的底部会产生不正确的反光。 一张曲率贴图即可屏蔽正面的毛孔反光，但这张表现毛孔的specular occlusion会极大程度地影响脸侧面的高光：将cavity乘入Fresnel项可以解决这个问题。 Reference NEXT GENERATION CHARACTER RENDERING

🕹️OpenGL丨魔方最终版！ - 魔方作业(5)
type: item
date: 2022-12-06T00:59:00+08:00
description: 前文提要—— 🕹️OpenGL丨RubikCube类搭建 - 魔方作业(1) 🕹️OpenGL丨Object Mouse Trackball - 魔方作业(2) 🕹️OpenGL丨利用模板缓冲实现鼠标拾取 - 魔方作业(3) 🕹️OpenGL丨魔方转动的动画与执行 - 魔方作业(4) 快交作业了，来最后把魔方作业完善一下，修了几个bug，然后再贴一下完整版的文档。 👇👇👇👇👇👇👇 要是播放不了戳这里 ☝☝☝☝☝☝☝ 交互相关 stencil残影 之前debug模板缓冲时，发现旋转会留下很多残影： 这是因为在glClear()命令前没有将glStencilMask()设置为0xFF，所以清空命令没有起效。 更正后的效果如下~ trackball转动过程中突然消失 在半边结构的作业中加入了trackball类的时候，一定程度上解决了这个问题。 为啥说一定程度呢...一方面不知道为啥，另一方面现在鼠标操作时偶尔会出现黑屏的闪烁..... 主要就是额外引入一个4x4矩阵来保存之前的旋转。在mouseClick()时拷贝matrix，用于计算local旋转轴和计算本次操作的的旋转矩阵。 void TrackBall::mouseMove(glm::vec2 pos2D) { glm::vec3 curpos = glm::vec3(0.0f); mapToSphere(pos2D, curpos); glm::vec3 axis_w = glm::cross(lastpos, curpos); glm::vec3 axis_l = glm::vec3(glm::inverse(lastmat) * glm::vec4(axis_w, 1)); float angle = glm::length(lastpos - curpos); matrix = glm::rotate(lastmat, angle, axis_l); } 歪门邪道网点shading 由于一个多月之前的我懒得读网格文件，这个魔方立方体的VAO是直接手写的，就没写法线的信息（而且直到刚刚才发现哦我没用indices...所以其实VAO里加一下就行.....）。最终是直接在着色器里歪门邪道地算了个world normal: vec3 N = step(vec3(0.499999),faceindex); N -= step(faceindex,vec3(-0.499999)); N = normalize(N); N = (modelmat*vec4(N,0.0)).xyz; emmm..... 没有任何参考价值。 Shaing根据光照和光线衰减大概分为三个区域，希望在中段的灰区绘制网点的效果： 一通调参之后就可以得到这样有点漫画感的效果~ 最终处理效果如图: 完整文档 实验原理 矩阵变换 三维模型的缩放、平移、旋转等操作都可以通过矩阵进行运算。 在本次实验中，使用了GLM库进行矩阵的管理和实现。 将物体绘制到屏幕上时，需要通过多次的坐标转换，一般的变换过程是[局部坐标]->[世界坐标]->[观察坐标]->[裁剪坐标]->[屏幕坐标]。 在魔方实验中，每个小立方体都是一组单独的网格，n x n x n个小立方体组成整个魔方。如果将整个魔方看为整体，那么我们还需要额外的一步坐标转换，将小立方体转换到"魔方空间"。 CUBE MATRIX保存每个小立方体各自的变形，包括其相对位置和相对旋转，也包括层旋转时的动画效果。 MODEL MATRIX保存整体魔方的旋转。 Trackball 使用TrackBall来实现鼠标对于物体的自由旋转，我们在屏幕外虚构一个球形曲面，使鼠标在二维屏幕上的移动投影到球形曲面上，以实现更佳的用户体验。 The motivation behind the trackball (aka arcball) is to provide an intuitive user interface for complex 3D object rotation via a simple, virtual sphere - the screen space analogy to the familiar input device bearing the same name. The sphere is a good choice for a virtual trackball because it makes a good enclosure for most any object; and its surface is smooth and continuous, which is important in the generation of smooth rotat…

🕹️OpenGL丨半边结构模型读取&显示[2]
type: item
date: 2022-12-05T16:30:00+08:00
description: 本应是Mesh系列的第一篇~为了赶动画原理拖了几天。 在上一篇中，我们用半边结构读取了网格，但实际上还没真正用到半边。 今天就是半边结构大显身手(?)的时候了！它可以快速找到每个顶点的所有邻居节点，以计算顶点的法线。 （入坑两年了，怎么还有人在写写了800次的基础卡渲.......) B类作业④ 实验报告 实验原理 Shading 使用半边结构读入obj格式的三维网格模型，通过叉乘计算顶点法线，使用Blinn-Phone模型进行简单的渲染。 Interaction 模型交互使用了A类作业①魔方程序中构建的TrackBall类，将鼠标在窗口内的位置移动映射到球面，计算球面角对模型进行旋转。 实验步骤 1.使用半边结构读入网格 详见B类作业③实验报告。 2.计算顶点法线（见Vertex::computeNormal()） 通过叉乘三角面的两个半边，我们可以获得三角面的法线。但顶点不止存在于一个三角面上，因此需要遍历所有其所在的三角面，对所有法线求平均值。 顶点不存储面相关的信息，因此遍历所有定点所在三角面是通过获取 所有顶点的出半边 实现的。通过outEdge->twin->next即可访问下一条出半边。 计算得到的法线可视化如图： 碎碎念：半边结构其实改了特别特别久。最开始没有成对创建半边，也没想起用哈希表，就分类讨论判断是否加新边之类的....改到头秃，最后索引关系还不对，算出来的法线： 像我的精神状态一样错乱...... 3.渲染 将灯光位置与相机位置传入着色器，通过与Position做差可得到视线向量viewDir与光线向量lightDir，取平均得到半程向量H = normalize(viewDir+lightDir).点乘法线即可进行简单的光照计算。 4.鼠标交互-旋转 引入了TrackBall类。 当鼠标在窗口按下时，通过回调函数click_callback将鼠标位置传入TrackBall，并用布尔值leftkey记录按键状态。 当鼠标在窗口移动时，若leftkey为真，通过回调函数mouse_callback将鼠标位置传入TrackBall，计算旋转角，并生成旋转矩阵，传递给vertex shader。 实现细节： 通过glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);开启鼠标监听 使用glfwGetCursorPos()获得鼠标的屏幕坐标，并重映射至[-1,1] TrackBall中用mat4 lasttrans记录先前的历史旋转。球面角计算得到的旋转轴是世界空间的，需要通过lasttrans的逆矩阵将旋转轴转换至物件空间。 5.鼠标交互-缩放 缩放的实现主要通过更改透视投影矩阵的视锥角实现。 当滚轮在窗口改变时，通过回调函数scroll_callback将滚轮增减作用于视锥角。 6.渲染补充 补充了简单的程序化背景和简单的卡通渲染效果。 在Blinn-Phone的光照基础上，对漫反射、高光的点乘计算值进行step()/smoothstep()即可获取清晰的光照边界。也可以添加ramp map以映射不同阶数的光照表现。 背景由一个的四边形面片构成，在片元着色器中计算颜色。 渐变：用TexCoords.y插值两种颜色。 晶格化渐变：以TexCoords.y为半径的sdf画圆。 将顶点朝法线方向偏移，即可获得描边效果。为刻画圈圈的网格结构，这里描边只绘制网格的背面，且绘制后不清理深度缓冲，这样在网格转折明显的部位仍可以出现描边效果。对比如下图： （Yeah 我知道还有让描边宽窄不变的trick🤪🤪但我懒得写了。这个case里摄像机根本就不能动。） 渲染时发现，附件提供的mesh模型缺少了一个三角面： （还以为法线又算错了 生气🤪 为了视觉效果，在obj文件中添加了这个面。因此实际提交的`eight.uniform.obj`比附件提供的文件要多一行~ 实验效果 通过鼠标左键旋转模型。 通过鼠标滚轮缩放模型。 按键盘1/2/3切换显示效果。 实验环境 Window 10. Visual Studio 2022. 依赖库：GLFW，GLAD, GLM

🕹️OpenGL丨半边结构模型读取&显示[1]
type: item
date: 2022-12-02T17:02:00+08:00
description: 定睛一看，已经12月了😵‍💫。 本来计划图形学基础总共写5个A类大作业，结果发现期末根本应付不过来。 看看剩下几个看着就秃头的A类作业（物理引擎、光追和网格加密&简化），还是先写B类把作业交上再说😪😪😪 这篇也是图形学的小作业，主要实现了半边结构网格的构建和读取。 实验原理 使用半边结构读入obj格式的三维网格模型，并配置VAO、EBO；通过glPolygenMode()设置点、线、面等显示模式。 半边结构 半边结构(The Half-Edge Data-Structure)是一种适用于流形多边形网格的数据结构。半边结构将一条边分解为两条半边(half-edge)，二者方向相反，各自属于相邻面的边环。其优点是各种查找的复杂度都只有O(1)。 半边结构中的顶点 Vertex 存储自身坐标 vec3(x,y,z) 存储出边(out-edge)指针 Halfedge* 出边一般为网格的边界。 在本次实验读取网格时，通过半边终点的出边查找twin半边是否存在。 半边结构中的半边 Halfedge 存储起点指针 Vertex* 存储所属面指针 Face* 存储上一条半边指针 Halfedge* 存储下一条半边指针 Halfedge* 存储相反的半边指针 Halfedge* 半边结构中面 Face 存储第一条半边指针 Halfedge* （图源cs184/284a） 点&线&面的绘制 通过glPolygonMode()实现了点(GL_POINT)、线(GL_LINE)、面(GL_FILL)绘制的切换。要点如下： 可编程管线下的shader配置：通过uniform vec3设置不同颜色，实现一shader多用； 点绘制：启用GL_VERTEX_PROGRAM_POINT_SIZE，可以于顶点着色器中通过gl_PointSize设置点模式下点的大小； 线绘制：作业样例中的网格显示只包含正面线框，但实验发现若在glPolygonMode()中配置只绘制正面（GL_FRONT)似乎不适用于可编程管线，因此通过启用GL_CULL_FACE以实现反面的剔除。 面+线绘制：先以GL_FILL模式绘制面，再以GL_LINE模式绘制线框。注意如果开启了深度测试，绘制的线会收到一定面片的遮挡，可以绘制线时关闭深度测试或将顶点向外进行一个小的偏移。 实验步骤 1.构造数据结构(见mesh.h) 除前面提到的Vertex、Halfedge、Face等结构外，EdgeKey结构用于构建哈希表，便于创建网格时查找半边是否已经存在。另外，Mesh类中indices、VAO、VBO、EBO用于可编程管线的网格绘制，不是半边结构的构成部分。 Mesh类中保存了顶点表、半边表和面表，其中后两者都是保存的指针，但顶点直接保存的结构体。这是因为struct的属性在内存中的存储顺序是连续的，可以直接将其作为AttributeArray设置给VAO，便于绘制。 2.读取文件(见Mesh::loadFile()) ​ 逐行读取obj文件。 obj 文件每一行为一条信息，开头字母代表信息类型； v 开头为顶点，三个值分别为x，y，z坐标； f 开头为顶点， 三个值为点索引。注意这里的索引值从1开始计数，注意使用时-1以防止数组越界。 void Mesh::loadFile(std::string path) { std::ifstream objFile; objFile.open(path); std::string line, keyword; std::stringstream stream; float x, y, z; int vert0, vert1, vert2; while (objFile && !objFile.eof()) { std::getline(objFile, line); if (line.size() == 0 || line[0] == '#' || isspace(line[0])) continue; stream.str(line); stream.clear(); stream >> keyword; //load vertex if (keyword == "v") { stream >> x >> y >> z; addVertex(x, y, z); } //load faces else if (keyword == "f") { stream >> vert0 >> vert1 >> vert2; addFace(--vert0, --vert1, --vert2); } } objFile.close(); setupMesh(); } 3.创建顶点(见Mesh::addVertex()) 首先通过坐标逐个创建顶点，出边oute…

ODOS丨好久不见，水个阿基米德螺线
type: item
date: 2022-11-28T17:23:00+08:00
description: 很久没写shadertoy了，碰巧OpenGL的一个小作业可以纯shader完成，拿来水一篇博客好啦。 阿基米德螺旋其实之前写过👉ODOS丨大大泡泡糖👈 相当于这次坐标系统里的y坐标。 实验原理 主要效果于Fragment Shader实现。阿基米德螺旋运用坐标转换，将纹理坐标从笛卡尔坐标系转换为类极坐标系，通过时间偏移形成动态效果。 阿基米德螺旋实现 - 坐标转换 当一点P沿动射线OP以等速率运动的同时，这射线又以等角速度绕点O旋转，点P的轨迹称为“阿基米德螺线”。它的极坐标方程为：r=a+b\theta，这种螺线的每条臂的间距永远相等于2\pi b。 通过构建基于阿基米德螺旋的坐标系，我们将一个点的纹理坐标(x,y)转换为(d,a):其中a为重映射后点P到原点的距离，d为点P到原点连线截取的螺旋弦长。 a的计算公式为： a= \frac{\sqrt{x^2 + y^2}}{R} - \frac{\theta _0}{2\pi}\\ \text{with }\theta _0 = atan(y,x) 其中R为阿基米德螺线的间距。则ceil(a)为转动的周数，完整的转动角为：\theta =\theta _0 + 2\pi ceil(a). d的计算公式为: q = \theta *\sqrt{|1-\theta^2|}\\ d = \frac{0.5R}{\pi}(q + log(q)) 对转换后的(d,a)坐标缩放后通过fract()函数截取小数部分，即可获取采样纹理的“局部坐标系”，坐标系的可视化效果如下： 小数部分的xy做为纹理坐标，用于采样star.bmp，d的整数部分作为生成随机颜色的seed. 用转换过的坐标系采样纹理，就可以得到沿阿基米德螺旋旋转的星星（如左下）。 为了更高的体现“旋转并扩散”的效果，利用上述的d整数部分通过step()制作了一张随时间旋转扩散的“蒙版”（如中下），与星星相乘后即可获取中心部分（如右下）。 随机颜色实现 Shader中的伪随机函数选用了最常见的正弦伪随机。将上文提到的d整数部分作为随机种子，并对RGB三个通道添加了不同的偏移值，以获取随机色。 float random (vec2 uv) { return fract(sin(dot(uv.xy, vec2(12.9898,78.233))) * 43758.5453123); } 颜色与上文的灰度蒙版相乘即可得到彩色星星。 存在的问题和掩盖方式 通过Shader完成该题目的方式的优点在于不需要维护粒子系统的产生、死亡，由于只有一个四边形所以顶点极少，属于偷懒的小trick...但极坐标的扭曲会导致靠近螺旋中心的星星产生比较明显的扇形变形。 为了掩饰中心变形，在中心叠加了一颗同系统一起旋转的星星，伪装为“粒子发射源”。 实验步骤 通过GLFW与GLAD构建自定义渲染管线，Shader文件的编译参考了LearnOpenGL的Shader类，纹理图片的读取使用stb_image库。 由于主要效果于片元着色器实现，主函数仅需一个绘制覆盖窗口的四边形，并将纹理坐标、时间与指定纹理传入着色器即可。 实验环境 Window 10. Visual Studio 2022. 依赖库：GLFW，GLAD，stb_image 渲染全屏quad 这里贴一下全屏方块的代码，方便以后ctrlC&ctrlV~ unsigned int quadVAO = 0; unsigned int quadVBO; void renderQuad() { if (quadVAO == 0) { float quadVertices[] = { // positions // texture Coords -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, }; // setup plane VAO glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &quadVBO); glBindVertexArray(quadVAO); glBindBuffer(GL_ARRAY_BUFFER, quadVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * si…

🕹️OpenGL丨可分离的次表面散射SSSS - 皮肤渲染(4)
type: item
date: 2022-11-15T18:19:00+08:00
description: Separable Subsurface Scattering 可分离的次表面散射 Separable Subsurface Scattering，简称SSSS，是较新的SSS算法(2015)。 通过水平和垂直卷积2个Pass来近似，效率更进一步提升，这是目前游戏里采用的主流技术，Unreal也对其进行了集成。 具体推导的过程我努力读了！但我读不懂！差太多数学没学了。。 简单来讲，是利用均匀介质反射剖面的镜像对称性，将传统的二维卷积通过奇异值分解(SVD)分解为一维卷积核。 对于我们这种数学菜鸟来说，其实可以直接用预计算好的核： SSSS_N_SAMPLES 11 float4 kernel[] = { float4(0.560479, 0.669086, 0.784728, 0), float4(0.00471691, 0.000184771, 5.07566e-005, -2), float4(0.0192831, 0.00282018, 0.00084214, -1.28), float4(0.03639, 0.0130999, 0.00643685, -0.72), float4(0.0821904, 0.0358608, 0.0209261, -0.32), float4(0.0771802, 0.113491, 0.0793803, -0.08), float4(0.0771802, 0.113491, 0.0793803, 0.08), float4(0.0821904, 0.0358608, 0.0209261, 0.32), float4(0.03639, 0.0130999, 0.00643685, 0.72), float4(0.0192831, 0.00282018, 0.00084214, 1.28), float4(0.00471691, 0.000184771, 5.07565e-005, 2), }; 根据不同的质量需求，这个核有多个版本，也可以用calculateKernel()生成。上文列举的是11次采样的核，最终作业里用的是25次采样的高质量版本。 后处理PASS 这里用了两个pass实现vertical、horizontal两个方向的卷积，两个pass的代码基本是一致的，只需要更改一下vec2 dir和渲染目标即可。 缩放比例 后处理除了之前的渲染结果外，还用到了深度纹理，以及视锥相关的fov、近远平面等信息，以获取片元线性的准确深度值。 （之前灯光视图的深度图使用的是正交投影，所以深度本来就是线性的，就不需要转化了~） // Fetch linear depth of current pixel: float depth = texture(temp_depth, TexCoords).r; depth = depth*2.0 - 1.0; //remap to NDC float linearDepth = (2.0 * near * far) / (far + near - depth * (far - near)); // Calculate the sssWidth scale (1.0 for a unit plane sitting on the // projection window): float distanceToProjectionWindow = 1.0 / tan(0.5 * radians(fov)); float scale = distanceToProjectionWindow / linearDepth; 卷积 利用已有的核，代码部分其实和一般的后处理差不多~ 注意offset后对深度进行了限制，如果offset后的深度变化过大，会lerp到原本的片元颜色，一定程度上避免结构线的模糊（当然ssss的总效果也会弱一些）。 // Calculate the final step to fetch the surrounding pixels: vec2 finalStep = SSSSwidth * scale * dir; finalStep *= sssstencil; // Modulate it using the alpha channel. finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. // Accumulate the center sample: vec4 colorBlurred = vec4(colorM,1.0); colorBlurred.rgb *= kernel[0].rgb; // Accumulate the other…

🕹️OpenGL丨ShadowDepthMap实现SSS透射 - 皮肤渲染(3)
type: item
date: 2022-11-15T11:02:00+08:00
description: 真实感皮肤渲染 本文的SSSS主要参考Jorge Jimenez的实现，在开始之前，先来回顾一下实时皮肤渲染的研究历程—— 首先来向毛老师学习： 《GPU Gems 3》：真实感皮肤渲染技术总结 对于皮肤渲染，我们需要使用BSSDF（Bidirectional Surface Scattering Reflectance Distribution Function，双向表面散射反射分布函数）。实时渲染的皮肤渲染过程由镜面反射和次表面散射两部分组成，我们主要关注SSS的部分。 Diffusion Profile 扩散剖面（diffusion profile）提供了光在高度散射的半透明材质表面下散射方式的近似。每种色光的扩散剖面不同，红光比绿色和蓝色散射得更远，因此强光照射下皮肤会产生橙红色的。 diffusion profile 到目前为止，图形学的科学家们偶极子与多级子等各种方法拟合了Diffusion Profile，实践表明多个高斯分布在一起可以对扩散剖面提供极好的近似。高斯函数同时是可分离的和径向对称的，可以相互卷积来产生新的高斯函数。 中间的推导看不懂(...)，我们就直接用科学家们的研究成果就好！ 预积分的皮肤着色（Pre-Integrated Skin Shading） 之前在wy的T星课上，实现过这个过程的极简版，即2010年的预积分的皮肤着色（Pre-Integrated Skin Shading），是把次表面散射的效果预计算成一张二维查找表，查找表的参数分别是dot(N,L)和曲率，因为这两者结合就能够反映出光照随着曲率的变化。 float3 R(float r) { return float3(0.233, 0.455, 0.649) * G(r, 0.0064) + float3(0.100, 0.336, 0.344) * G(r, 0.0484) + float3(0.118, 0.198, 0 ) * G(r, 0.187 ) + float3(0.113, 0.007, 0.007) * G(r, 0.567 ) + float3(0.358, 0.004, 0 ) * G(r, 1.99 ) + float3(0.078, 0 , 0 ) * G(r, 7.41 ); } 注意上面贴出的这种图是tonemapping&gamma后的结果，使用时要注意转换。 预积分很适合移动端，因为实时的计算量非常小，直接采样即可~ Real-Time Realistic Skin Translucency 本文的SSSS即SSS透射主要参考Jorge Jimenez的实现。 Real-Time Realistic Skin Translucency →论文原文 Jorge的方法在《GPU Gems 3》里半透明阴影贴图（Translucent Shadow Maps，TSMs）的透射计算方法(2007)的基础上进一步进行了提升(2010)，但竟然也是12年前的算法了。 像耳朵、鼻孔等较薄的地方受强光照射时，会有明显偏红的透射现象，前面说的种种SSS方法有一定效果但不够理想。有些方法会预计算模型的厚度贴图，作为控制SSS效果的参数。但预计算时不能得知光线的方向，这样求得的厚度其实是不准确的。Jorge提供了一种使用ShadowMap来获取光线方向厚度的方法。 下图中的红线是Jorge的算法，蓝线为《GPU Gems 3》中较早的算法，由于只需要读取ShadowMap的深度值，减少了数据的存储量~ 先来复习一下灯光利用shadowMap的方式，这与我们厚度的算法其实是非常相似的~ ShadowMap或者DepthMap，是从光线空间对场景的预渲染，存储了光线视角下场景的最小深度Zin。我们将[WorldSpace→LightSpace]这个转换矩阵也一并传入着色器，就可以取得片元在光线空间的坐标，它到光源的距离即是在光线视角的深度Zout。如果Zout小于Zin，那么就说明这个片元与光源之间有遮挡，它应该处于阴影中。 而Zin和Zout的差，其实就是光线穿过物体的厚度！ 获取shadow map 在具体实现上，首先我们需要一个从光线视角的Pass，只需写入深度。这里的VertexShader进行场景的坐标转换，而Fragment什么都不用做。 // 1. shadow pass: depth mapping from light space for shading and thickness calculations // ----------------------------------------------------------------- glViewport(0, 0, 1024, 1024); g…

🕹️OpenGL丨延迟渲染&帧缓冲 - 皮肤渲染(2)
type: item
date: 2022-11-15T10:01:00+08:00
description: 帧缓冲 Framebuffer 帧缓冲有点类似于引擎中的渲染目标Render Target，可以让用户自定义颜色缓冲、深度缓冲和模板缓冲。 在默认的设置状况下，我们的渲染操作都是在默认帧缓冲的渲染缓冲上进行的，GLFW帮我们配置好了这个环境，即glBindFramebuffer(GL_FRAMEBUFFER, 0)。 我们可以通过glGenFramebuffers()来创建自己的帧缓冲，同纹理等一样，创建后要绑定到当前目标GL_FRAMEBUFFER来对它进行接下来的操作。 通过if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)可以检查帧缓冲是否完整可用。那么如何确定一个帧缓冲是完整的呢？ 附加至少一个缓冲（颜色、深度或模板缓冲）。 至少有一个颜色附件(Attachment)。 所有的附件都必须是完整的（保留了内存）。 每个缓冲都应该有相同的样本数。 附件 Attachment 附件是一个内存位置，它能够作为帧缓冲的一个缓冲。附件有两种类型：纹理(Texture)或渲染缓冲对象(RenderbufferObject). 纹理附件 Texture as Attachment 创建纹理附件的方式和创建纹理基本相同，注意在data参数传入NULL，即可在创建时仅分配内存，后序渲染时再写入数据。 创建后要将其附加到帧缓冲： unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); 如果是深度缓冲，类型和格式应该设置为GL_DEPTH_ATTACHMENT和GL_DEPTH_COMPONENT. 具体参数可以参考LearnOpenGL中的教程。 渲染缓冲对象附件 Renderbuffer Object as Attachment 渲染缓冲对象的数据储存是OpenGL原生的渲染格式，它是只写的，只能通过glReadPixels()来读取。 它在写入或复制数据到其他缓冲时是更快的，如果不需要采样（如深度和模板），可以选择渲染缓冲对象来进行优化。 怎么选呢？举个栗子 如果希望结合延迟渲染和前向渲染，在前向渲染时需要获取先前的深度值以便深度测试，就适合用Renderbuffer Object来传递深度值。 但在阴影计算中，我们需要用纹理坐标对灯光空间的深度图进行采样，就适合用Texture来传递深度值。 渲染到纹理 当进行后处理或延迟缓冲时，我们需要渲染到纹理，再对渲染纹理采样。这个步骤在OpenGL中是这样的： [Pass 1] 将新的帧缓冲绑定为激活的帧缓冲 和往常一样渲染场景 [Pass 2] 绑定默认的帧缓冲 绘制一个横跨整个屏幕的四边形，将帧缓冲的颜色缓冲作为它的纹理。 我们需要为Pass2单独准备一套后处理的Shader，可以搬这个轮子： VertexShader： #version 330 core layout (location = 0) in vec2 aPos; layout (location = 1) in vec2 aTexCoords; out vec2 TexCoords; void main() { TexCoords = aTexCoords; gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } FragmentShader： #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D screenTexture; void main() { vec3 col = texture(screenTexture, TexCoords).rgb; //some post process operations HERE FragColor = vec4(col, 1.0); }…