当前位置: 首页 > news >正文

OpenGL学习笔记(Blinn-Phong、伽马矫正、阴影)

目录

  • Blinn-Phong
  • Gamma矫正
    • Gamma
    • Gamma矫正
    • 实现方法
    • sRGB纹理
    • 衰减
  • 阴影
    • shadow mapping
    • 渲染阴影
    • 改进阴影贴图
    • PCF

GitHub主页:https://github.com/sdpyy1
OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppLearn/tree/main/OpenGL

Blinn-Phong

PhongShading不仅对真实光照有很好的近似,而且性能也很高。但是它的镜面反射会在一些情况下出现问题,特别是物体反光度很低时,会导致大片(粗糙的)高光区域。下面这张图展示了当p为1.0时地板会出现的效果:
在这里插入图片描述
上图高光区域的边缘有明显的断层,出现这个问题的原因是观察向量和反射向量间的夹角不能大于90度。如果点积的结果为负数,镜面光分量会变为0.0。所以90°的地方明显的断层
下图就是这个角度大于90°的情况
在这里插入图片描述
1977年,James F. Blinn在风氏着色模型上加以拓展,引入了Blinn-Phong着色模型。Blinn-Phong模型与风氏模型非常相似,但是它对镜面光模型的处理上有一些不同,让我们能够解决之前提到的问题。Blinn-Phong模型不再依赖于反射向量,而是采用了所谓的半程向量(Halfway Vector),即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近时,镜面光分量就越大。
在这里插入图片描述
用光线与实现夹角的半程向量与法线夹角来表示镜面光强度。当视线正好与(现在不需要的)反射向量对齐时,半程向量就会与法线完美契合。所以当观察者视线越接近于原本反射光线的方向时,镜面高光就会越强。现在,不论观察者向哪个方向看,半程向量与表面法线之间的夹角都不会超过90度(除非光源在表面以下)。

Blinn-Phong与风氏模型唯一的区别就是,Blinn-Phong测量的是法线与半程向量之间的夹角,而风氏模型测量的是观察方向与反射向量间的夹角。
在这里插入图片描述
除此之外还有一个小区别,就是半程向量与表面法线的夹角通常会小于观察与反射向量的夹角。所以,如果你想获得和风氏着色类似的效果,就必须在使用Blinn-Phong模型时将镜面反光度设置更高一点。通常我们会选择风氏着色时反光度分量的2到4倍。
在这里插入图片描述
Blinn-Phong着色的一个附加好处是,它比Phong着色性能更高,因为我们不必计算更加复杂的反射向量了。

Gamma矫正

Gamma

设备输出亮度 = 输入电压的Gamma次幂
在这里插入图片描述
第一行表示人眼感知色阶,第二行是物理的色阶。
人眼对暗色的分辨能力远超亮色,那么在有限的计算机颜色中(256个色阶),亮色和暗色均匀分布的话,那亮色部分就会精度过剩而暗色部分就会精度不足。如何解决这个问题?进行 Gamma 矫正。

人的视觉系统对光照强度的反应并不是线性的,这意味着一个线性变化的光强,人眼感知起来却并非如此。

在这里插入图片描述
y=x的电线表示Gamma为1的理想状态,当我们输出一个值为(0.5,0.0,0.0)的颜色时,显示器输出的实际颜色为(0.218,0.0,0.0) 就是0.5的2.2次幂。如果我们把设置的颜色翻倍变为(1,0,0)实际上显示器的输出翻了4.5倍不止。

这块我感觉他解释的很乱,不如直接说人眼的gamma是1/2.2,显示器的gamma是2.2,刚好抵消了

为什么看起来"正常":
人眼恰好需要约4.5倍的物理亮度变化才能感知"2倍亮度"
显示器的Gamma 2.2 ≈ 补偿了人眼的非线性感知
最终达到"所见即所得"的效果

因为颜色是根据显示器的输出配置的,所以线性空间中的所有中间(照明)计算在物理上都是不正确的。随着更多先进的照明算法的引入,这一点变得更加明显,如下图所示:
在这里插入图片描述
如果没有适当地纠正这个显示器伽马,照明看起来是错误的,艺术家将很难获得逼真和好看的结果。解决方案正是应用伽马校正。

Gamma矫正

Gamma校正(Gamma Correction)的思路是在最终的颜色输出到显示器之前先将Gamma的倒数作用到颜色上。就是让一个颜色变得更亮之后输出到显示器又会变暗,刚好抵消了~

我们来看另一个例子。还是那个暗红色(0.5,0.0,0.0)。在将颜色显示到显示器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在显示器上相当于降低了2.2
次幂的亮度,所以倒数就是1/2.2次幂。Gamma校正后的暗红色就会成为(0.73,0.0,0.0)。校正后的颜色接着被发送给显示器,最终显示出来的颜色是(0.5,0.0,0.0)。你会发现使用了Gamma校正,显示器最终会显示出我们在应用中设置的那种线性的颜色。

基于gamma2.2的颜色空间叫做sRGB颜色空间。每个显示器的gamma曲线都有所不同,但是gamma2.2在大多数显示器上表现都不错。

实现方法

  1. 使用OpenGL内建的sRGB帧缓冲。
    开启即可
glEnable(GL_FRAMEBUFFER_SRGB);

不开矫正效果
在这里插入图片描述
开启矫正
在这里插入图片描述
明显亮了很多,这也就是在抵消显示器让颜色变暗的情况。
使用这种方法要注意,他是输出到屏幕之前进行的矫正,如果你提前进行了矫正,那后续操作就全错了。

  1. 自己在像素着色器中进行gamma校正w。
void main()
{// 在线性空间做炫酷的光照效果[...]// 应用伽马矫正float gamma = 2.2;fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}

sRGB纹理

显示器显示颜色时使用了gamma曲线(约2.2),这意味着:你看到的颜色 ≠ 内存中的数值
在这里插入图片描述
典型错误场景:
当你在渲染管线中:
① 加载sRGB纹理(已经gamma校正)
② 进行光照计算(应该在线性空间)
③ 输出到显示器(又自动gamma校正)
→ 实际发生了两次gamma校正 → 画面过亮

也就是说sRGB纹理它已经被gamma矫正过了,渲染完成后不需要再进行gamma矫正。

所以可以先转为线性颜色计算后,再统一进行gamma矫正
为了转为线性的颜色,可以使用特定的纹理创建函数

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

这个函数会自动把sRGB纹理转为线性空间,正常进行计算后统一进行gamma矫正并输出

衰减

伽马校正的另一个不同之处在于光照衰减。在真实的物理世界中,光衰减与光源距离的平方成反比。

float attenuation = 1.0 / (distance * distance);

然而当我们使用这个衰减公式时,衰减效果总是过于强烈
如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成 ( 1.0 / d i s t a n c e 2 ) 2.2 (1.0/distance^2)^{2.2} (1.0/distance2)2.2,所以进行了强烈的衰减。

总而言之,gamma校正使你可以在线性空间中进行操作。因为线性空间更符合物理世界,大多数物理公式现在都可以获得较好效果,比如真实的光的衰减。你的光照越真实,使用gamma校正获得漂亮的效果就越容易。这也正是为什么当引进gamma校正时,建议只去调整光照参数的原因。

阴影

shadow mapping

这个在我的tinyrenderer中实现过。这里额外提到了把摄像机角度生成的zbuffer存储在纹理中。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图。
首先创建一个有三个正方形的场景

int main(){// 初始化窗口GLFWwindow * window = InitWindowAndFunc();stbi_set_flip_vertically_on_load(true);// 启用深度测试glEnable(GL_DEPTH_TEST);// shaderShader shader("./shader/base.vert", "./shader/base.frag");// 纹理unsigned int planeDiffuse = loadTexture("./assets/diffuse.png");unsigned int cubeDiffuse = loadTexture("./assets/container.jpg");// 地板float planeVertices[] = {// positions            // normals         // texcoords25.0f, -0.5f,  25.0f,  0.0f, 1.0f, 0.0f,  25.0f,  0.0f,-25.0f, -0.5f,  25.0f,  0.0f, 1.0f, 0.0f,   0.0f,  0.0f,-25.0f, -0.5f, -25.0f,  0.0f, 1.0f, 0.0f,   0.0f, 25.0f,25.0f, -0.5f,  25.0f,  0.0f, 1.0f, 0.0f,  25.0f,  0.0f,-25.0f, -0.5f, -25.0f,  0.0f, 1.0f, 0.0f,   0.0f, 25.0f,25.0f, -0.5f, -25.0f,  0.0f, 1.0f, 0.0f,  25.0f, 25.0f};// plane VAOunsigned int planeVBO,planeVAO;glGenVertexArrays(1, &planeVAO);glGenBuffers(1, &planeVBO);glBindVertexArray(planeVAO);glBindBuffer(GL_ARRAY_BUFFER, planeVBO);glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));glBindVertexArray(0);while (!glfwWindowShouldClose(window)){// 清理窗口glClearColor(0.05f, 0.05f, 0.05f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);glm::mat4 view = camera.GetViewMatrix();;shader.use();glBindVertexArray(planeVAO);shader.setMat4("projection", projection);shader.setMat4("view", view);shader.setMat4("model", glm::mat4(1.0));shader.setInt("diffuse_texture1", 0);glBindTexture(GL_TEXTURE_2D, planeDiffuse);glDrawArrays(GL_TRIANGLES, 0, 6);glBindTexture(GL_TEXTURE_2D, cubeDiffuse);// cubesglm::mat4 model = glm::mat4(1.0f);model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(0.0f, 1.5f, 0.0));model = glm::scale(model, glm::vec3(0.5f));shader.setMat4("model", model);renderCube();model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(2.0f, 0.0f, 1.0));model = glm::scale(model, glm::vec3(0.5f));shader.setMat4("model", model);renderCube();model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(-1.0f, 0.0f, 2.0));model = glm::rotate(model, glm::radians(60.0f), glm::normalize(glm::vec3(1.0, 0.0, 1.0)));model = glm::scale(model, glm::vec3(0.25));shader.setMat4("model", model);renderCube();// 事件处理glfwPollEvents();// 双缓冲glfwSwapBuffers(window);processFrameTimeForMove();processInput(window);}glfwTerminate();return 0;
};void renderCube()
{// initialize (if necessary)if (cubeVAO == 0){float vertices[] = {// back face-1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 0.0f, 0.0f, // bottom-left1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 1.0f, 1.0f, // top-right1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 1.0f, 0.0f, // bottom-right1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 1.0f, 1.0f, // top-right-1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 0.0f, 0.0f, // bottom-left-1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f, 0.0f, 1.0f, // top-left// front face-1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f, 0.0f, // bottom-left1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f, 0.0f, // bottom-right1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f, 1.0f, // top-right1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f, 1.0f, // top-right-1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f, 1.0f, // top-left-1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f, 0.0f, // bottom-left// left face-1.0f,  1.0f,  1.0f, -1.0f,  0.0f,  0.0f, 1.0f, 0.0f, // top-right-1.0f,  1.0f, -1.0f, -1.0f,  0.0f,  0.0f, 1.0f, 1.0f, // top-left-1.0f, -1.0f, -1.0f, -1.0f,  0.0f,  0.0f, 0.0f, 1.0f, // bottom-left-1.0f, -1.0f, -1.0f, -1.0f,  0.0f,  0.0f, 0.0f, 1.0f, // bottom-left-1.0f, -1.0f,  1.0f, -1.0f,  0.0f,  0.0f, 0.0f, 0.0f, // bottom-right-1.0f,  1.0f,  1.0f, -1.0f,  0.0f,  0.0f, 1.0f, 0.0f, // top-right// right face1.0f,  1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 1.0f, 0.0f, // top-left1.0f, -1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 0.0f, 1.0f, // bottom-right1.0f,  1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 1.0f, 1.0f, // top-right1.0f, -1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 0.0f, 1.0f, // bottom-right1.0f,  1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 1.0f, 0.0f, // top-left1.0f, -1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 0.0f, 0.0f, // bottom-left// bottom face-1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f, 0.0f, 1.0f, // top-right1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f, 1.0f, 1.0f, // top-left1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f, 1.0f, 0.0f, // bottom-left1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f, 1.0f, 0.0f, // bottom-left-1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f, 0.0f, 0.0f, // bottom-right-1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f, 0.0f, 1.0f, // top-right// top face-1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f, 0.0f, 1.0f, // top-left1.0f,  1.0f , 1.0f,  0.0f,  1.0f,  0.0f, 1.0f, 0.0f, // bottom-right1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f, 1.0f, 1.0f, // top-right1.0f,  1.0f,  1.0f,  0.0f,  1.0f,  0.0f, 1.0f, 0.0f, // bottom-right-1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f, 0.0f, 1.0f, // top-left-1.0f,  1.0f,  1.0f,  0.0f,  1.0f,  0.0f, 0.0f, 0.0f  // bottom-left};glGenVertexArrays(1, &cubeVAO);glGenBuffers(1, &cubeVBO);// fill bufferglBindBuffer(GL_ARRAY_BUFFER, cubeVBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// link vertex attributesglBindVertexArray(cubeVAO);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);}// render CubeglBindVertexArray(cubeVAO);glDrawArrays(GL_TRIANGLES, 0, 36);glBindVertexArray(0);
}

在这里插入图片描述
下来就是生成阴影贴图了

第一步需要使用自定义帧缓冲生成深度贴图

GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

下面生成一张空纹理图,因为我们只关心深度值,我们要把纹理格式指定为GL_DEPTH_COMPONENT

    GLuint depthMap;glGenTextures(1, &depthMap);glBindTexture(GL_TEXTURE_2D, depthMap);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

把纹理图挂载到自定义帧缓冲中
然而,不包含颜色缓冲的帧缓冲对象是不完整的,所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。我们通过将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

这里光源视角下设置的是平行光,所以直接用正交投影即可

GLfloat near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);

下面为了渲染是从光源角度的渲染,所以要修改视图矩阵

glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

两者结合就得到了一个光空间的变换矩阵,他能将每个世界空间坐标转变为光源视角下的坐标

用于渲染的shader非常简单

#version 330 core
layout (location = 0) in vec3 position;uniform mat4 lightSpaceMatrix;
uniform mat4 model;void main()
{gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}

片段着色器什么都不用干,但是它默认就会进行深度测试,会存储深度缓冲

#version 330 corevoid main()
{// gl_FragDepth = gl_FragCoord.z;
}

在自定义缓冲下进行渲染后,就存入了深度缓存的信息

        // 切换到光源视角下glm::mat4 lightProjection, lightView;glm::mat4 lightSpaceMatrix;float near_plane = 1.0f, far_plane = 7.5f;lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);lightView = glm::lookAt(lightPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));lightSpaceMatrix = lightProjection * lightView;depthShader.use();depthShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);glClear(GL_DEPTH_BUFFER_BIT);// cubesglm::mat4 model = glm::mat4(1.0f);model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(0.0f, 1.5f, 0.0));model = glm::scale(model, glm::vec3(0.5f));depthShader.setMat4("model", model);renderCube();model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(2.0f, 0.0f, 1.0));model = glm::scale(model, glm::vec3(0.5f));depthShader.setMat4("model", model);renderCube();model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(-1.0f, 0.0f, 2.0));model = glm::rotate(model, glm::radians(60.0f), glm::normalize(glm::vec3(1.0, 0.0, 1.0)));model = glm::scale(model, glm::vec3(0.25));depthShader.setMat4("model", model);renderCube();glBindFramebuffer(GL_FRAMEBUFFER, 0);

在这里插入图片描述

渲染阴影

首先在顶点着色器,我们要记录每个顶点在光源视角下的位置FragPosLightSpace

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;out vec2 TexCoords;out VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;vec4 FragPosLightSpace;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;void main()
{// 世界坐标vs_out.FragPos = vec3(model * vec4(aPos, 1.0));vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;vs_out.TexCoords = aTexCoords;// 在光源视角下的裁剪空间坐标vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);gl_Position = projection * view * model * vec4(aPos, 1.0);
}

在片段着色器中,通过是否在阴影中来修改是否需要高光和漫反射光,如果在阴影中,则设置为0

#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;vec4 FragPosLightSpace;
} fs_in;uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap;uniform vec3 lightPos;
uniform vec3 viewPos;float ShadowCalculation(vec4 fragPosLightSpace)
{[...]
}void main()
{           vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;vec3 normal = normalize(fs_in.Normal);vec3 lightColor = vec3(1.0);// Ambientvec3 ambient = 0.15 * color;// Diffusevec3 lightDir = normalize(lightPos - fs_in.FragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * lightColor;// Specularvec3 viewDir = normalize(viewPos - fs_in.FragPos);vec3 reflectDir = reflect(-lightDir, normal);float spec = 0.0;vec3 halfwayDir = normalize(lightDir + viewDir);  spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);vec3 specular = spec * lightColor;    // 计算阴影float shadow = ShadowCalculation(fs_in.FragPosLightSpace);       vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    FragColor = vec4(lighting, 1.0f);
}

计算方法如下
首先根据像素位置对应记录的光源坐标下的NDC坐标(注意要手动进行透视除法,切换到[0,1]范围。在深度贴图中用NDC坐标当作UV坐标去采样,得到该点最近的深度值,然后与该点的深度值进行比较

float ShadowCalculation(vec4 fragPosLightSpace)
{// 手动进行透视除法,转为NDC坐标vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;// 变换到[0,1]的范围projCoords = projCoords * 0.5 + 0.5;// 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标)float closestDepth = texture(shadowMap, projCoords.xy).r; // 取得当前片段在光源视角下的深度float currentDepth = projCoords.z;// 检查当前片段是否在阴影中  大离的近float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;return shadow;
}

最终渲染出来为
在这里插入图片描述

改进阴影贴图

目前的渲染是有问题的
在这里插入图片描述
地板四边形渲染出一大块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne),下图解释了成因:

在这里插入图片描述
阴影贴图受限于分辨率,在离光源很远的情况下,多个片段在深度贴图的同一个位置进行了采样。
我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。
在这里插入图片描述

    float bias = 0.005;float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;

还有更好的方法是自适应的偏移量

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

但是当偏移量过大时,也会出现悬浮效果。这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上
在这里插入图片描述
下边这幅是正常的,很明显能感觉到上图的错误
在这里插入图片描述

我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。

因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。

glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // 不要忘记设回原先的culling face

另外还有一个问题是光锥不可见的区域一律被任务处于阴影中了
在这里插入图片描述

也就是说有些纹理坐标超出了深度贴图的范围,我们可以修改贴图的环绕参数,将超出部分的深度值全部设置为1,这样,贴图外永远都不在阴影中

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

但这样处理只解决了一块阴影

在这里插入图片描述

仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。

当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER环绕方式不起作用,因为我们把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。

直接强制让z值大于1的位置(在远平面外的点)设置不在阴影中
在这里插入图片描述

PCF

目前的阴影表面仍有锯齿
在这里插入图片描述

因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。

你可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。

另一个(并不完整的)解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。

float ShadowCalculation(vec4 fragPosLightSpace)
{// perform perspective dividevec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;// Transform to [0,1] rangeprojCoords = projCoords * 0.5 + 0.5;// Get closest depth value from light's perspective (using [0,1] range fragPosLight as coords)float closestDepth = texture(shadowMap, projCoords.xy).r;// Get depth of current fragment from light's perspectivefloat currentDepth = projCoords.z;// Calculate bias (based on depth map resolution and slope)vec3 normal = normalize(fs_in.Normal);vec3 lightDir = normalize(lightPos - fs_in.FragPos);float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);// Check whether current frag pos is in shadow// float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;// PCFfloat shadow = 0.0;vec2 texelSize = 1.0 / textureSize(shadowMap, 0);for(int x = -1; x <= 1; ++x){for(int y = -1; y <= 1; ++y){float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += currentDepth - bias > pcfDepth  ? 1.0 : 0.0;}}shadow /= 9.0;// Keep the shadow at 0.0 when outside the far_plane region of the light's frustum.if(projCoords.z > 1.0)shadow = 0.0;return shadow;
}

在这里插入图片描述
正交 vs 投影
在渲染深度贴图的时候,正交(Orthographic)和投影(Projection)矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
在这里插入图片描述
如果使用的是透视投影,在使用深度贴图时,需要先将深度转为线性深度

float LinearizeDepth(float depth)
{float z = depth * 2.0 - 1.0; // Back to NDC return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}

后续对于阴影的优化放在Games202课程作业一中进行~

相关文章:

OpenGL学习笔记(Blinn-Phong、伽马矫正、阴影)

目录 Blinn-PhongGamma矫正GammaGamma矫正实现方法sRGB纹理衰减 阴影shadow mapping渲染阴影改进阴影贴图PCF GitHub主页&#xff1a;https://github.com/sdpyy1 OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdp…...

GPLT-2025年第十届团体程序设计天梯赛总决赛题解(2025天梯赛题解,266分)

今天偶然发现天梯赛的代码还保存着&#xff0c;于是决定写下这篇题解&#xff0c;也算是复盘一下了 L1本来是打算写的稳妥点&#xff0c;最后在L1-6又想省时间&#xff0c;又忘记了insert&#xff0c;replace这些方法怎么用&#xff0c;也不想花时间写一个文件测试&#xff0c…...

day4 pandas学习

%pip install openxyxl 找一个自己觉得有意思的文件。我找的是成绩单来玩。 这节学的比较耗时了&#xff0c;大概用了60分钟。 import pandas as pd data2 pd.read_csv(rD:\python代码区\代码随想录挑战-调试区\python训练营\1_计算类专业分流学生成绩排名.csv) #print(data)…...

【Java学习笔记】循环结构

循环结构 一、for循环 for循环结构 for(循环变量初始化;循环条件;循环变量迭代){循环操作&#xff08;可以多条语句&#xff09; }for循环写死循环 for(;;){语句 }注意点&#xff1a;循环变量的初始化在for语句内&#xff0c;属于是局部变量&#xff0c;在全局中会出现未定义…...

URP-UGUI交互功能实现

一、非代码层面实现交互&#xff08;SetActive&#xff09; Button &#xff1a;在OnClick&#xff08;&#xff09;中添加SetActive方法&#xff08;但是此时只首次有效&#xff09; Toggle &#xff1a;在OnClick&#xff08;&#xff09;中添加动态的SetActive方法 &#…...

08-IDEA企业开发工具-集成AI插件通义灵码

需要登陆才可使用&#xff01;&#xff01;&#xff01; 1. 安装AI编程插件 找到插件: 在IDEA的设置中&#xff0c;找到插件&#xff08;Plugins&#xff09;部分。安装插件: 搜索“通义灵码”&#xff0c;找到后点击安装&#xff08;Install&#xff09;&#xff0c;接受条款…...

解决报错:this[kHandle] = new _Hash(algorithm, xofLen);

前端项目编译报错&#xff1a; node:internal/crypto/hash:68this[kHandle] new _Hash(algorithm, xofLen);^Error: error:0308010C:digital envelope routines::unsupportedat new Hash (node:internal/crypto/hash:68:19)at Object.createHash (node:crypto:138:10)at modu…...

使用 Streamlit 打造一个简单的照片墙应用

在现代 web 开发中&#xff0c;快速构建交互式应用是一项重要的技能。Streamlit 是一个强大的 Python 库&#xff0c;允许开发者以最小的代码量创建美观且功能丰富的 web 应用。今天&#xff0c;我们将通过分析一段简单的 Streamlit 代码&#xff0c;展示如何构建一个照片墙应用…...

深度学习优化器和调度器的选择和推荐

一、常用优化器对比 1. 随机梯度下降&#xff08;SGD&#xff09; 原理&#xff1a;每次迭代使用小批量数据计算梯度并更新参数。优点&#xff1a;实现简单&#xff0c;适合大规模数据集。缺点&#xff1a;收敛速度慢&#xff0c;容易陷入局部最优或鞍点。适用场景&#xff1…...

“时间”,在数据处理中的真身——弼马温一般『无所不能』(DeepSeek)

电子表格时间处理真理&#xff1a;数值存储最瘦身&#xff0c;真身闯关通四海。 笔记模板由python脚本于2025-04-23 22:25:59创建&#xff0c;本篇笔记适合喜欢在电子表格中探求时间格式的coder翻阅。 【学习的细节是欢悦的历程】 博客的核心价值&#xff1a;在于输出思考与经验…...

为什么Spring中@Bean注解默认创建单例Bean

在Spring框架中&#xff0c;使用Bean注解定义的对象默认确实是单例的&#xff0c;这是由Spring容器的设计哲学和实际需求决定的。下面我从多个角度解释这一设计选择的原因和机制。 1. Spring Bean作用域基础 Spring定义了多种Bean作用域&#xff0c;其中默认是单例(Singleton…...

GPLT-2025年第十届团体程序设计天梯赛总决赛题解(2025天梯赛题解,共计266分)

今天偶然发现天梯赛的代码还保存着&#xff0c;于是决定写下这篇题解&#xff0c;也算是复盘一下了 L1本来是打算写的稳妥点&#xff0c;最后在L1-6又想省时间&#xff0c;又忘记了insert&#xff0c;replace这些方法怎么用&#xff0c;也不想花时间写一个文件测试&#xff0c…...

JDK(Ubuntu 18.04.6 LTS)安装笔记

一、前言 本文与【MySQL 8&#xff08;Ubuntu 18.04.6 LTS&#xff09;安装笔记】同批次&#xff1a;先搭建数据库&#xff0c;再安装JDK&#xff0c;后面肯定就是部署Web应用&#xff1a;典型的单机部署。“麻雀虽小五脏俱全”&#xff0c;善始善终&#xff0c;还是记下来吧。…...

Java 拦截器完全指南:原理、实战与最佳实践

一、引言 拦截器的基本概念 在现代 Java Web 开发中&#xff0c;拦截器&#xff08;Interceptor&#xff09;是一种用于在请求处理前后插入自定义逻辑的机制。简单来说&#xff0c;它是一种“横切逻辑处理器”&#xff0c;可以用来对请求进行预处理、后处理&#xff0c;甚至终…...

2025.04.23华为机考第二题-200分

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 02. 魔法彩灯森林 问题描述 在卢小姐的魔法花园中,有一棵神奇的彩灯树。这棵树的每个节点都装有一盏魔法灯,灯有三种颜色状态:红色(用数字1表示)、绿色(用数字2表示)和蓝色(…...

【Leetcode 每日一题】1399. 统计最大组的数目

问题背景 给你一个整数 n n n。请你先求出从 1 1 1 到 n n n 的每个整数 10 10 10 进制表示下的数位和&#xff08;每一位上的数字相加&#xff09;&#xff0c;然后把数位和相等的数字放到同一个组中。 请你统计每个组中的数字数目&#xff0c;并返回数字数目并列最多的组…...

系统重装——联想sharkbay主板电脑

上周给一台老电脑重装系统系统&#xff0c;型号是lenovo sharkbay主板的电脑&#xff0c;趁着最近固态便宜&#xff0c;入手了两块长城的固态&#xff0c;装上以后插上启动U盘&#xff0c;死活进不去boot系统。提示 bootmgr 缺失&#xff0c;上网查了许久&#xff0c;终于解决了…...

Python数据清洗笔记(上)

一、数据清洗概述 数据清洗是数据分析过程中至关重要的一步&#xff0c;约占整个数据分析过程的60%-80%的时间。主要包括处理缺失值、异常值、重复值、格式不一致等问题。 二、常用工具 主要使用Python的Pandas库进行数据清洗&#xff1a; import pandas as pd import nump…...

三、Python编程基础03

目录 一、debug 调试的使用1. 打断点2. 右键 Debug 运行代码3. 单步执行代码,查看过程 二、字符串1、定义与下标引用2、切片3、查找4、去除空白字符5、转换大小写与拆分6、其他方法-替换、连接、是否为纯数字7、登录案例优化 三、列表 list1、列表基础操作2、案例&#xff1a; …...

西门子S7-200SMART 控制Profinet闭环步进MD-4250-PN (1)电机及专栏介绍

一、前言 本系列是我继 《西门子S7-1200PLC 控制步进电机 MD-4240-PN》系列专栏后&#xff0c;新开的一篇专栏。 系列的主题围绕 S7-200SMART Profinet闭环步进(MD-4250-PN) 触摸屏的硬件&#xff0c;预计作四篇文章&#xff0c;分别为&#xff1a;专栏介绍、硬件介绍、PLC…...

NoSQL 简单讲解

目录 1. NoSQL 的背景与意义 1.1 数据库的演变 1.2 NoSQL 的兴起 2. NoSQL 数据库的分类 2.1 键值存储&#xff08;Key-Value Stores&#xff09; 2.2 文档数据库&#xff08;Document Stores&#xff09; 2.3 列族存储&#xff08;Column-Family Stores&#xff09; 2.…...

TCP 协议:原理、机制与应用

一、引言 在当今数字化的时代&#xff0c;网络通信无处不在&#xff0c;而 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;作为互联网协议栈中的核心协议之一&#xff0c;扮演着至关重要的角色。无论是浏览网页、发送电子邮件还是进行文件…...

C++23 新特性:令声明顺序决定非静态类数据成员的布局 (P1847R4)

文章目录 引言背景知识非静态类数据成员类的内存布局 P1847R4提案内容示例代码 影响和优势提高代码的可预测性与其他语言和库的交互更加方便简化代码调试和优化 编译器支持情况实际应用场景嵌入式系统开发跨语言编程内存优化 总结 引言 在C的发展历程中&#xff0c;每一个新版…...

Java 环境配置详解(Windows、macOS、Linux)

Java 环境配置是学习和开发 Java 程序的第一步&#xff0c;也是至关重要的一步。一个正确配置的 Java 环境能够保证你的 Java 程序能够顺利编译、运行和调试。本文将详细介绍在 Windows、macOS 和 Linux 三种主流操作系统上配置 Java 环境的步骤&#xff0c;力求详尽、易懂&…...

ChatBEV:一种理解 BEV 地图的可视化语言模型

25年3月来自上海交大、上海AI实验室、同济大学和MAGIC的论文“ChatBEV: A Visual Language Model that Understands BEV Maps”。 交通场景理解对于智能交通系统和自动驾驶至关重要&#xff0c;可确保车辆安全高效地运行。虽然 VLM 的最新进展已显示出整体场景理解的前景&…...

利用Python爬虫实现百度图片搜索的PNG图片下载

在图像识别、训练数据集构建等场景中&#xff0c;我们经常需要从互联网上批量下载图片素材。百度图片是中文搜索中最常用的来源之一。本文将介绍如何使用Python构建一个稳定、可扩展的百度图片爬虫&#xff0c;专门用于下载并保存高清PNG格式图片。 一、项目目标 本项目的目标…...

自主可控鸿道Intewell工业实时操作系统

鸿道Intewell工业实时操作系统是东土科技旗下科东软件自主研发的新一代智能工业操作系统&#xff0c;以下是相关介绍&#xff1a; 系统架构 -Intewell-C全实时构型&#xff1a;设备上只运行自研RTOS的全实时系统&#xff0c;适用于有功能安全认证需求的实时控制场景&#xf…...

【数据结构入门训练DAY-21】信息学奥赛一本通T1334-围圈报数

文章目录 前言一、题目二、解题思路结语 前言 本次训练内容 STL库中队列的使用练习。训练解题思维 一、题目 有&#xff4e;个人依次围成一圈&#xff0c;从第&#xff11;个人开始报数&#xff0c;数到第&#xff4d;个人出列&#xff0c;然后从出列的下一个人开始报数&am…...

【C语言】C语言中的字符函数和字符串函数全解析

前言 在C语言编程中&#xff0c;字符和字符串的处理是必不可少的。C语言标准库提供了丰富的字符和字符串函数&#xff0c;这些函数极大地简化了字符和字符串的操作。本文将详细介绍这些函数的使用方法、模拟实现以及一些实用的代码示例&#xff0c;帮助你更好地理解和掌握它们…...

声纹振动传感器在电力监测领域的应用

声纹振动传感器在电力监测领域有多种应用&#xff0c;主要包括以下几个方面&#xff1a; 变压器监测 故障诊断&#xff1a;变压器在运行过程中会产生特定的声纹和振动信号&#xff0c;当变压器内部出现故障&#xff0c;如绕组短路、铁芯松动、局部放电等&#xff0c;其声纹和振…...

配色之道:解码产品设计中的UI设计配色艺术

在数字化时代&#xff0c;用户界面&#xff08;UI&#xff09;作为产品与用户交互的桥梁&#xff0c;其设计质量直接影响着用户体验与产品成败。而配色&#xff0c;作为UI设计中最为直观且富有表现力的元素之一&#xff0c;不仅是视觉美学的体现&#xff0c;更是情感传递、信息…...

python基础语法测试

1. 关于Python语言数值操作符&#xff0c;以下选项中描述错误的是 A x%y表示x与y之商的余数&#xff0c;也称为模运算 B x/y表示x与y之商 C x**y表示x的y次幂&#xff0c;其中&#xff0c;y必须是整数 D x//y表示x与y之整数商&#xff0c;即不大于x与y之商的最大整数2. 下面代码…...

如何安装Visio(win10)

首先下载下面这些文件 HomeStudent2021Retail.img officedeploymenttool_17531-20046.exe office中文语言包.exe 确保这些文件都在一个文件夹内&#xff08;我已经上传这些资源&#xff0c;这些资源都是官网下载的&#xff09; 官网资源下载教程 1.下载Office镜像&#xff0…...

Sql刷题日志(day5)

面试&#xff1a; 1、从数据分析角度&#xff0c;推荐模块怎么用指标衡量&#xff1f; 推荐模块主要目的是将用户进行转化&#xff0c;所以其主指标是推荐的转化率推荐模块的指标一般都通过埋点去收集用户的行为并完成相应的计算而形成相应的指标数据&#xff0c;而这里的驱动…...

.NET、java、python语言连接SAP系统的方法

💡 本文会带给你 可用哪些技术与Sap系统连接怎样用Rfc技术连接SAP一. SAP系统与外部系统集成技术 SAP系统提供了多种方式供Java、.NET、Python等外部编程语言进行连接和集成。 1. RFC (Remote Function Call) 连接 适用语言:Java, .NET, Python, 其他支持RFC的编程语言 …...

C++ 容器查找效率

C 容器查找效率 只要选对容器&#xff0c;多写几行代码就能让程序“飞”起来。下面用生活化的比喻 足够多的带注释示例&#xff0c;帮你弄懂常用 STL 容器的查找特性。 读完你应该能快速判断&#xff1a;“我的场景该用哪一个&#xff1f;” 0. 先把“查找复杂度”聊明白 记号…...

汽车可变转向比系统的全面认识

一、什么是转向比&#xff1f; 转向比又叫转向传动比&#xff0c;是指方向盘转向角度与车轮转向角度之比。 例如&#xff0c;方向盘向左转动了60角&#xff0c;而车轮则向左转动了30角&#xff0c;转向比就是2&#xff1a;1。 转向比越大&#xff0c;意味着要使车轮转向达到指…...

知识储备-后仿

仿真环境设定 mem、constant input(scan/test)等设非x初值无复位ff通过force-release处理vcs timing_check、optconfigfile (自定义配置&#xff0c;如指定模块timing check与否&#xff09;设置运行核数、仿真精度不要过小设置、根据测试目的选择性关闭、dump范围(时间/空间)…...

C# AutoResetEvent 详解

一、简介 AutoResetEvent 是 .NET 中一个重要的线程同步原语&#xff0c;用于线程间的信号通知。下面我将从多个方面详细讲解 AutoResetEvent。 AutoResetEvent 是 System.Threading 命名空间下的一个类&#xff0c;它表示一个线程同步事件&#xff0c;在等待线程被释放后会自…...

【水印图片文字识别】水印相机拍摄的照片提取重要的信息可以批量改名,批量识别水印文字内容批量给图片改名,基于QT和腾讯OCR的识别方案

应用场景 在日常工作和生活中,人们使用水印相机拍摄的照片往往包含重要的信息,如拍摄地点、时间、事件等。这些信息以水印的形式存在于照片中。当需要对大量照片进行管理时,手动为每张照片重命名是一项繁琐且容易出错的工作。通过批量识别水印文字内容并为图片改名,可以提…...

【架构】Armstrong公理系统通俗详解:数据库设计的基本法则

关系数据库就像一本精心设计的通讯录&#xff0c;而Armstrong公理系统则是帮我们整理这本通讯录的基本规则。本文将用简单易懂的语言和生活实例&#xff0c;带你理解这套看似复杂的理论。 1. 什么是函数依赖&#xff1f; 想象你有一个学生信息表&#xff0c;包含学号、姓名、…...

Redis高频核心面试题

1.阐述Redis的主要的特性和优势 &#xff1f; 【Redis 的主要特性】 &#xff08;1&#xff09;Redis 是完全开源免费的&#xff0c;遵守 BSD 协议&#xff0c;是一个高性能的 key-value 数据库 &#xff08;2&#xff09;Redis 与其他 key - value 缓存产品有以下三个特点&a…...

Vue3-原始值的响应式方案ref

一、原始类型的值 原始类型的指的是: boolean、number、string、symbol、undefind和null等类型的值. 一、初识ref 为什么vue3需要对原始值的响应式做单独处理?因为Javascript中的Proxy只能代理对象类型的数据, 如普通对象、数组、Set、Map等。 为了解决Proxy不能代理原始类…...

VUE的创建

Vue Vue的创建脚手架创建Vue的解析setup函数:插值表达式数据响应式 ⽬录和⽂件解读指令 Vue的创建 下载VScode https://code.visualstudio.com/download 加入拓展包 点击 然后输入代码 <!DOCTYPE html> <html lang"en"><head><meta charset&…...

第51讲:AI在农业政策支持系统中的应用——用人工智能点亮科学决策的新范式

目录 🧠 开篇引导:农业决策,如何更科学? 🤖 什么是“AI驱动的农业政策支持系统”? 🧪 案例解析:AI如何助力农业政策? 🌾 案例一:政策补贴的智能匹配 🌍 案例二:土地利用规划支持 🛠 AI在农业政策建模中的常用技术 📈 可视化与接口建议 🌟 未来…...

开关电源LM5160-Q1 在 Fly-Buck 电路中的硬件设计与 PCB Layout 优化

一、LM5160-Q1 规格书深度解读与硬件设计参数提取 核心功能 宽输入范围:4.5V~65V,支持汽车级输入电压波动(AEC-Q100 标准,温度等级 1:-40C~125C)。 集成度:内置高侧 / 低侧 MOSFET,无需外部肖特基二极管,同步降压 / Fly-Buck 双模式。 控制架构:自适应恒定导通时间…...

面向 C# 初学者的完整教程

&#x1f9f1; 一、项目结构说明 你的项目大致结构如下&#xff1a; TaskManager/ ├── backend/ │ ├── TaskManager.Core/ // 实体类和接口 │ ├── TaskManager.Infrastructure/ // 数据库、服务实现 │ └── TaskManager.API/ // We…...

Python实现孔填充与坐标转换

一、问题背景 在工业自动化、材料加工等领域&#xff0c;常需要在图像识别的闭合区域内生成等间距的孔位坐标。本文基于OpenCV库&#xff0c;提出一种从图像边界提取到物理坐标生成的完整解决方案&#xff0c;实现以下核心功能&#xff1a; 像素坐标到实际尺寸的转换安全间距…...

精益数据分析(16/126):掌握关键方法,探寻创业真谛

精益数据分析&#xff08;16/126&#xff09;&#xff1a;掌握关键方法&#xff0c;探寻创业真谛 大家好&#xff01;在创业与数据分析的学习道路上&#xff0c;每一次的探索都让我们离成功更近一步。今天&#xff0c;我带着和大家共同进步的初心&#xff0c;继续深入解读《精…...

pytorch(gpu版本安装)

Pytorch官网下载很慢 选择以下方法&#xff0c;关于版本对应从pytorch官网查看 官网方法 pip install torch2.2.0 torchvision0.17.0 torchaudio2.2.0 --index-url https://download.pytorch.org/whl/cu121 其他方法 pip install torch2.2.0cu121 torchvision0.17.0cu121 t…...