LearnOpenGL——法线贴图、视差贴图学习笔记
LearnOpenGL——法线贴图、视差贴图学习笔记
- 法线贴图 Normal Mapping
- 一、基本概念
- 二、切线空间
- 1. TBN矩阵
- 2. 切线空间中的法线贴图
- 三、复杂模型
- 四、小问题
- 视差贴图 Parallax Mapping
- 一、基本概念
- 二、实现视差贴图
- 三、陡峭视差映射 Steep Parallax Mapping
- 四、视差遮蔽映射 Parallax Occlusion Mapping
法线贴图 Normal Mapping
一、基本概念
通过调整每个曲面的法向量,来让光照变化,进而模拟凹凸不平的表面。为使法线贴图工作,我们需要为每个fragment提供一个法线。我们可以使用2D纹理来存储法线数据,然后通过采样来得到特定纹理的法向量。
将法线向量的x、y、z元素储存到纹理中,代替颜色的r、g、b元素,因为法线向量的范围在[-1,1],所以我们要先将其映射到[0,1],变换为RGB颜色元素。
vec3 rgb_normal = normal * 0.5 + 0.5;
法线贴图多是蓝色为主,是因为法线基本上以z轴正方向为主:存储为B分量(蓝色)。法线向量从z轴方向也有向其他方向的偏差,颜色也就发生了轻微的变化。
加载纹理,绑定到合适的纹理单元,然后将片元着色器中添加对法线贴图的采样。
uniform sampler2D normalMap; void main()
{ // 从法线贴图范围[0,1]获取法线normal = texture(normalMap, fs_in.TexCoords).rgb;// 将法线向量转换为范围[-1,1]normal = normalize(normal * 2.0 - 1.0); [...]// 像往常那样处理光照
}
目前,如果我们让平面竖直面对我们,此时效果正常,因为法线贴图中的法线方向指向z正方向并且平面的法线也指向z轴正方向。但当我们移动旋转平面时,就会发现光照不正确。比如下图,因为此时平面法线方向为y轴正方向,但法线贴图中的方向仍然为z轴正方向。
解决办法:在一个不同的坐标空间中处理所有的光照——切线空间:
这个坐标空间中的法线贴图矢量总是指向z轴正方向,然后其他照明矢量(如光源方向、观察方向等)相对于这个z方向进行变换。这样法线贴图不需要根据物体的方向变化而变化。无论物体如何旋转,光照计算都能在切线空间中正确处理,这简化了计算过程。
二、切线空间
切线空间是位于三角形表面上的空间,法线相对于单个三角形的局部坐标系。可以看成法线贴图向量的局部坐标系。无论最终变换到什么方向,它们都指向z轴正方向。我们可以使用一个特殊的矩阵来将法线贴图中的法线向量从切线空间变换到世界或观察空间,使它们与表面的法线方向对齐。
1. TBN矩阵
Tangent正切、Bitangent双切、Normal法向量。
为了构造这个矩阵,我们需要向上N、向右T、向前B三个向量。目前我们已知向上的向量N。接下来我们将会推导计算T和B的过程。(需要一点数学基础)
我们发现法线贴图的T和B坐标跟纹理的UV坐标很相似,我们可以从这里入手。(因为纹理坐标和切线向量在同一空间中)
U就是T坐标,V就是B坐标,不难发现
然后我们将上述方程组写成矩阵乘法
然后左右两边都乘上UV矩阵的逆矩阵
现在难点就是计算UV矩阵的逆矩阵(可以用伴随矩阵来求解逆矩阵,不过对于2×2的矩阵,我们可以直接写)
来个代码例子:
目前我们有两个三角形123和134,我们挑选其中一个三角形来计算。我们只需为每个三角形计算一个切线/副切线,它们对于每个三角形上的顶点都是一样的。
// positions
glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// texture coordinates
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
// normal vector
glm::vec3 nm(0.0, 0.0, 1.0);
我们计算第一个三角形的 E 和 deltaUV
glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;
然后就可以根据上面的公式来计算tangent和bitangent
tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
tangent1 = glm::normalize(tangent1);bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
bitangent1 = glm::normalize(bitangent1); [...] // 对平面的第二个三角形采用类似步骤计算切线和副切线
2. 切线空间中的法线贴图
为了让法线贴图工作,我们需要创建一个TBN矩阵,我们可以将之前计算的切线和副切线传给顶点着色器。然后在main中创建TBN矩阵
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;void main()
{[...]vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));vec3 B = normalize(vec3(model * vec4(bitangent, 0.0)));vec3 N = normalize(vec3(model * vec4(normal, 0.0)));mat3 TBN = mat3(T, B, N)
}
有两种使用TBN矩阵的办法
- 直接使用TBN矩阵:
将TBN矩阵传给片元着色器,并使用TBN矩阵将法线向量从切线空间传到世界空间。让法线与其他光照变量处于同一空间。因为法线贴图中的法线向量是在切线空间中的,而其他光照矢量是在世界空间中的。
out VS_OUT {vec3 FragPos;vec2 TexCoords;mat3 TBN;
} vs_out; void main()
{[...]vs_out.TBN = mat3(T, B, N);
}
在片元着色器中我们用mat3作为输入变量
in VS_OUT {vec3 FragPos;vec2 TexCoords;mat3 TBN;
} fs_in;
然后将采样的法线贴图来转换(先采样,再映射,再转换)
normal = texture(normalMap, fs_in.TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0);
normal = normalize(fs_in.TBN * normal);
- 使用TBN的逆矩阵,将所有世界空间向量转换到切线空间中计算
vs_out.TBN = transpose(mat3(T, B, N));
我们这里使用的是transpose是因为TBN是正交矩阵,正交矩阵的转置和逆矩阵相等。在shader中,使用逆矩阵的开销比转置大。
然后将TBN逆矩阵传给片元着色器,将其他变量都转换为切线空间进行计算,法线向量不做变换。
void main()
{ vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;normal = normalize(normal * 2.0 - 1.0); vec3 lightDir = fs_in.TBN * normalize(lightPos - fs_in.FragPos);vec3 viewDir = fs_in.TBN * normalize(viewPos - fs_in.FragPos); [...]
}
我们可以不用在片元着色器中进行转换,我们可以直接在顶点着色器中,对lightPos、viewPos以及FragPos进行变换,这样就可以免去在片元着色器中的操作了。也可以节省开销,因为顶点着色器运行次数比片元着色器少。(以下是在顶点着色器中)
out VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;uniform vec3 lightPos;
uniform vec3 viewPos;[...]void main()
{ [...]mat3 TBN = transpose(mat3(T, B, N));vs_out.TangentLightPos = TBN * lightPos;vs_out.TangentViewPos = TBN * viewPos;vs_out.TangentFragPos = TBN * vec3(model * vec4(position, 0.0));
}
在像素着色器中我们使用这些新的输入变量来计算切线空间的光照。因为法线向量已经在切线空间中了,光照就有意义了。
glm::mat4 model;
model = glm::rotate(model, (GLfloat)glfwGetTime() * -10, glm::normalize(glm::vec3(1.0, 0.0, 1.0)));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
RenderQuad();
三、复杂模型
对于复杂的模型,Assimp加载器已经帮我们实现了为每个顶点计算出柔和的切线和副切线向量。我们可以通过下面的代码用Assimp获取计算出来的切线空间:
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.Tangent = vector;
当加载模型时,Assimp的aiTextureType_NORMAL并不会加载它的法线贴图,而aiTextureType_HEIGHT却能
vector normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
四、小问题
对于网格很大的模型,上面有很多共享的顶点,法线贴图应用到这些表面时会讲切线向量平均化。但是这样的话TBN可能不会相互垂直,因此TBN可能不再是正交矩阵了,法线贴图就会稍稍偏移。
我们可以对其进行格拉姆-施密特正交化,对TBN进行重正交化。在顶点着色器中:
vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(normal, 0.0)));
// re-orthogonalize T with respect to N
T = normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B = cross(T, N);mat3 TBN = mat3(T, B, N)
视差贴图 Parallax Mapping
一、基本概念
视差贴图是和法线贴图类似,也是用来增加表面细节而不需要额外增加几何信息。它对根据储存在纹理中的几何信息对顶点进行位移或偏移。每个纹理像素包含了高度值的纹理叫做高度贴图
视差贴图是根据观察方向和高度图来改变纹理坐标。
红色线表示高度图中的值,V是观察方向。视差贴图目的是在A位置上的片元不再使用A的纹理坐标,而是使用B的纹理坐标。
如何从点A得到点B的纹理坐标:视差贴图通过A片元的高度值来缩放观察方向V。我们将V的长度缩放为等于A处高度 H(A),然后我们确定P向量,作为纹理坐标偏移量。这个点B得到的还是近似值,当高度快速变化的时候,看起来就不会很真实。
我们在旋转之后,点P就很难定位了,所以仿照法线贴图,我们引入了切线空间来计算。我们将观察方向变化到切线空间中,所以P向量的x和y分量会与表面切线和副切线对齐,由于切线和副切线向量与表面纹理坐标的方向相同,我们可以用P的x和y元素作为纹理坐标的偏移量。
二、实现视差贴图
这个例子的高度图的颜色是相反的,我们叫他深度贴图,模拟深度比高度更容易一些。
这个时候,我们使用向量V减去A的纹理坐标得到P。在着色器中,我们使用1-采样得到的深度贴图中的深度值。
位移贴图是在像素着色器中实现的,我们需要得到观察方向V,所以需要切线空间中的观察者位置和片元位置。
顶点着色器如下
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;out VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;uniform vec3 lightPos;
uniform vec3 viewPos;void main()
{gl_Position = projection * view * model * vec4(position, 1.0f);vs_out.FragPos = vec3(model * vec4(position, 1.0)); vs_out.TexCoords = texCoords; vec3 T = normalize(mat3(model) * tangent);vec3 B = normalize(mat3(model) * bitangent);vec3 N = normalize(mat3(model) * normal);mat3 TBN = transpose(mat3(T, B, N));vs_out.TangentLightPos = TBN * lightPos;vs_out.TangentViewPos = TBN * viewPos;vs_out.TangentFragPos = TBN * vs_out.FragPos;
}
在片元着色器中,我们实现视差贴图的逻辑
#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} fs_in;uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform sampler2D depthMap;uniform float height_scale;vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);void main()
{ // Offset texture coordinates with Parallax Mappingvec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);vec2 texCoords = ParallaxMapping(fs_in.TexCoords, viewDir);// then sample textures with new texture coordsvec3 diffuse = texture(diffuseMap, texCoords);vec3 normal = texture(normalMap, texCoords);normal = normalize(normal * 2.0 - 1.0);// proceed with lighting code[...]
}
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ float height = texture(depthMap, texCoords).r; vec2 p = viewDir.xy / viewDir.z * (height * height_scale);return texCoords - p;
}
我们定义了一个ParallaxMapping函数来获得纹理坐标。在此函数中,我们先从深度图中采样到深度值,然后计算偏移p,同时引入了一个height_scale来控制视差效果的强度。
为什么要用viewDir.xy / viewDir.z: 通过除以 viewDir.z,我们确保了视角接近平行于表面(即 viewDir.z 接近0),偏移量 p 会更大。这模拟了当一个物体从边缘观察时,由于视差效应,你能够看到的物体部分与直接正面观察时不同的现象。
此时视差贴图的边缘仍然有古怪的现象,原因是在平面的边缘上,纹理坐标超出了0到1的范围进行采样,根据纹理的环绕方式导致了不真实的结果。解决的方法是当它超出默认纹理坐标范围进行采样的时候就丢弃这个fragment:
texCoords = ParallaxMapping(fs_in.TexCoords, viewDir);
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)discard;
我们会发现在一些极端的视角,还是会有明显的走样。
三、陡峭视差映射 Steep Parallax Mapping
相比于正常的视差贴图,陡峭视差贴图用更多的样本点来确定向量P到B,所以即使陡峭的高度变化,由于提高了样本数量,效果也会不错。
陡峭视差贴图的思想是将总深度划分为多个相等深度的层,然后对于每一层都对深度图进行采样,沿着P方向移动纹理坐标,直到找到一个采样深度值小于当前层的深度值
我们需要修改一下ParallaxMapping函数
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ // number of depth layersconst float numLayers = 10;// calculate the size of each layerfloat layerDepth = 1.0 / numLayers;// depth of current layerfloat currentLayerDepth = 0.0;// the amount to shift the texture coordinates per layer (from vector P)vec2 P = viewDir.xy * height_scale; vec2 deltaTexCoords = P / numLayers;vec2 currentTexCoords = texCoords;float currentDepthMapValue = texture(depthMap, currentTexCoords).r;while(currentLayerDepth < currentDepthMapValue){// shift texture coordinates along direction of PcurrentTexCoords -= deltaTexCoords;// get depthmap value at current texture coordinatescurrentDepthMapValue = texture(depthMap, currentTexCoords).r; // get depth of next layercurrentLayerDepth += layerDepth; }return currentTexCoords;
}
- 首先设置层数,然后用1除以层数得到每层的深度值
- 初始化currentLayerDepth(当前层深度值)
- 然后计算得到P,再用P除以层数,将P也分层,得到分层后的纹理坐标
- 再初始化当前纹理坐标的深度值
- 开始循环比较,若当前层的深度值 < 当前纹理坐标的深度值,就继续下一层,直到当前层深度值 > 当前纹理坐标的深度值,就停止循环,返回此时纹理坐标
我们再改进一下,当视角方向是垂直表面时,就不需要太多采样点,当视角方向偏向侧面时,就增大采样点
const float minLayers = 8;
const float maxLayers = 32;
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
陡峭视差贴图同样有自己的问题。因为这个技术是基于有限的样本数量的,我们会遇到锯齿效果以及图层之间有明显的断层。
四、视差遮蔽映射 Parallax Occlusion Mapping
与陡峭视差映射差不多,但我们不采用碰撞后的第一个深度层的纹理坐标,而是在碰撞前和碰撞后的深度层之间进行线性插值。线性插值的权重取决于表面高度与两个深度层值之间的距离。
我们还是修改ParallaxMapping代码
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ // number of depth layersconst float minLayers = 10;const float maxLayers = 20;float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir))); // calculate the size of each layerfloat layerDepth = 1.0 / numLayers;// depth of current layerfloat currentLayerDepth = 0.0;// the amount to shift the texture coordinates per layer (from vector P)vec2 P = viewDir.xy / viewDir.z * height_scale; vec2 deltaTexCoords = P / numLayers;// get initial valuesvec2 currentTexCoords = texCoords;float currentDepthMapValue = texture(depthMap, currentTexCoords).r;while(currentLayerDepth < currentDepthMapValue){// shift texture coordinates along direction of PcurrentTexCoords -= deltaTexCoords;// get depthmap value at current texture coordinatescurrentDepthMapValue = texture(depthMap, currentTexCoords).r; // get depth of next layercurrentLayerDepth += layerDepth; }// -- parallax occlusion mapping interpolation from here on// get texture coordinates before collision (reverse operations)vec2 prevTexCoords = currentTexCoords + deltaTexCoords;// get depth after and before collision for linear interpolationfloat afterDepth = currentDepthMapValue - currentLayerDepth;float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;// interpolation of texture coordinatesfloat weight = afterDepth / (afterDepth - beforeDepth);vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);return finalTexCoords;
}
相关文章:

LearnOpenGL——法线贴图、视差贴图学习笔记
LearnOpenGL——法线贴图、视差贴图学习笔记 法线贴图 Normal Mapping一、基本概念二、切线空间1. TBN矩阵2. 切线空间中的法线贴图 三、复杂模型四、小问题 视差贴图 Parallax Mapping一、基本概念二、实现视差贴图三、陡峭视差映射 Steep Parallax Mapping四、视差遮蔽映射 P…...

界面优化 - 绘图
目录 1. 基本概念 2. 绘制各种形状 2.1 绘制线段 2.2 绘制矩形 2.3 绘制圆形 2.4 绘制文本 2.5 设置画笔 2.6 设置画刷 3. 绘制图片 3.1 绘制简单图片 3.2 平移图片 3.3 缩放图片 3.4 旋转图片 1. 基本概念 虽然 Qt 已经内置了很多的控件, 但是不能保证现有控件就…...

死锁问题分析和解决——资源回收时
1.描述问题 在完成线程池核心功能功能时,没有遇到太大的问题(Any,Result,Semfore的设计),在做线程池资源回收时,遇到了死锁的问题 1、在ThreadPool的资源回收,等待线程池所有线程退出时ÿ…...

【Java】效率工具模板的使用
Java系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 文章目录 Java系列文章目录一、前言二、学习内容:三、问题描述四、解决方案:4.1 乱码问题4.2 快捷键模板4.3 文件模板 一、前言 提高效率 二、学习内容&am…...
c++指南 -指针和引用
指针和引用 指针的基本概念 指针是存储另一个变量的内存地址的变量。指针变量的声明包括指针类型和星号 (*)。 int* ptr; // ptr 是一个指向 int 类型的指针指针操作 初始化:将指针设置为变量的地址。 int var 10; int* ptr &var; // ptr 现在存储 var 的…...

[CISCN 2023 华北]ez_date
[CISCN 2023 华北]ez_date 点开之后是一串php代码: <?php error_reporting(0); highlight_file(__FILE__); class date{public $a;public $b;public $file;public function __wakeup(){if(is_array($this->a)||is_array($this->b)){die(no array);}if( (…...

前端不同项目使用不同的node版本(Volta管理切换)
前端不同项目使用不同的node版本(Volta管理切换) 使用volta自动切换前端项目的node版本, 每个不同的前端项目,可以使用不同的node版本。Volta这个工具,它允许用户方便地安装、切换和管理不同版本的Node.js,避免了为每个项目手动配…...

Ropdump:针对二进制可执行文件的安全检测工具
关于Ropdump Ropdump是一款针对二进制可执行文件的安全检测工具,该工具基于纯Python开发,是一个命令行工具,旨在帮助广大研究人员检测和分析二进制可执行文件中潜在的ROP小工具、缓冲区溢出漏洞和内存泄漏等安全问题。 功能介绍 1、识别二进…...

Quartz - 定时任务框架集成
参考了若依框架,将quartz定时任务框架集成到自己的项目当中。 目录 一、Quartz概述二、库表创建1.Quartz关键表(11张)表SQL 2.自定义业务表(2张)表SQL 三、代码示例1.依赖引入2.类文件1)定时任务配置类2&am…...

GoModule
GOPATH 最早的就是GOPATH构建模式, go get下载的包都在path中的src目录下 src目录是源代码存放目录。 package mainimport ("net/http""github.com/gorilla/mux" )func main() {r : mux.NewRouter()r.HandleFunc("/hello", func(w h…...
SQL - 数据库管理
保障数据库安全的用户账户和权限问题,当在工作环境中使用MySQL的时候,我们需要创建其他用户账户,并赋予它们特定权限。创建一个用户 create user wolf127.0.0.1 identified by 1234; create user wolf127.0.0.1 identified by 1234;-- 无 …...
密码学之AES算法
文章目录 1. AES简介1.1 AES算法的历史背景1.2 AES算法的应用领域 2. AES加解密流程图2. AES算法原理2.1 AES加密过程2.2 AES解密过程 1. AES简介 1.1 AES算法的历史背景 AES算法,全称为Advanced Encryption Standard(高级加密标准)&#x…...
GitHub每日最火火火项目(8.20)
项目名称:goauthentik / authentik 项目介绍:authentik 是一款提供认证功能的工具,它就像是一个强大的粘合剂,能够满足您在认证方面的各种需求。无论是在安全验证、用户身份管理还是访问控制等方面,它都能发挥重要作用…...
(五)Flink Sink 数据输出
经过上面的 Transformation 操作之后,最终形成用户所需要的结果数据集。通常情况下,用户希望将结果数据输出到外部存储介质或者传输到下游的消息中间件中,在 Flink 中,将 DataStream 数据输出到外部系统的过程被定义为 Sink 操作。 目录 (一)基本数据输出 (二)第三方…...
Spring 注入、注解及相关概念补充
一、Spring DI 的理解 DI ( Dependency Inject,中文释义:依赖注入)是对 IOC 概念不同角度的描述,是指应用程序在运行时,每一个 bean 对象都依赖 IOC 容器注入到当前 bean 对象所需要的另一个 bean 对象。(例如…...
【Linux多线程】线程安全的单例模式
文章目录 1. 单例模式 与 设计模式1.1 单例模式1.2 设计模式1.3 饿汉实现模式 与 懒汉实现模式1.4 饿汉模式① 饿汉模式的特点② 饿汉式单例模式的实现③ 饿汉式单例模式的优缺点④ 适用场景 1.5 懒汉模式① 懒汉式单例模式的特点② 懒汉式单例模式的实现③ 懒汉式单例模式的优…...

基于jqury和canvas画板技术五子棋游戏设计与实现(论文+源码)_kaic
摘 要 网络五子棋游戏如今面临着一些新的挑战和机遇。一方面,网络游戏需要考虑到网络延迟和带宽等因素,保证游戏的实时性和稳定性。另一方面,网络游戏需要考虑到游戏的可玩性和趣味性,以吸引更多的玩家参与。本文基于HTML5和Canv…...

指针 (四)
一 . 指针的使用和传值调用 (1)strlen 的模拟实现 库函数 strlen 的功能是求字符串长度,统计的是字符串中 \0 之前的字符个数,函数原格式如下: 我们的参数 str 接收到一个字符串的起始地址,然后开始统计…...

便利店(超市)管理系统设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家,服务很多代码文档,百分百好评,战绩可查!!入职于互联网大厂,可以交流,共同进步。有保障的售后 代码参考数据库参…...

Excel中的“块”操作
在Excel中,有offset、index、indirect三个对“区域”操作的函数,是较高版本Excel中“块”操作的利器。 (笔记模板由python脚本于2024年08月20日 19:25:21创建,本篇笔记适合喜欢用Excel处理数据的coder翻阅) 【学习的细节是欢悦的历程】 Pytho…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)
升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点,但无自动故障转移能力,Master宕机后需人工切换,期间消息可能无法读取。Slave仅存储数据,无法主动升级为Master响应请求ÿ…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...