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…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...
jmeter聚合报告中参数详解
sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample(样本数) 表示测试中发送的请求数量,即测试执行了多少次请求。 单位,以个或者次数表示。 示例:…...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...