【Overload游戏引擎细节分析】standard材质Shader
提示:Shader属于GPU编程,难写难调试,阅读本文需有一定的OpenGL基础,可以写简单的Shader,不适合不会OpenGL的朋友
一、Blinn-Phong光照模型
Blinn-Phong光照模型,又称为Blinn-phong反射模型(Blinn–Phong reflection model)或者 phong 修正模型(modified Phong reflection model),是由 Jim Blinn于 1977 年在文章中对传统 phong 光照模型基础上进行修改提出的。它是一个经验模型,并不完全符合真实世界中的光照现象,但由于实现起来简单方便,并且计算速度和得到的效果都还不错,因此在早期被广泛的使用。
相对于Phong模型,Blinn-Phong是对高光部分进行简化计算,对于环境光、漫反射计算是一样的。环境光、漫反射一般处理如下:
- 环境光:是光线经过周围环境表面多次反射后形成的,利用它可以描述一块区域的亮度,在光照模型中,通常用一个常量来表示;
- 漫反射:当光线照射到一个点时,该光线会被均匀的反射到各个方向,这种反射称为漫反射。也就是说,在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为漫反射光在任何反射方向上的分布都是一样的,一般可使用Lambert余弦定律计算。
- 高光反射(Specular): 也称镜面光,若物体表面很光滑,当平行入射的光线射到这个物体表面时,仍会平行地向一个方向反射出来。
高光计算
直接上结论,因为这个模型资料很多,大家可以参考https://zhuanlan.zhihu.com/p/442023993

h = l + v ∣ l ∣ + ∣ v ∣ h=\frac{l+v}{\left | l \right | + \left | v \right | } h=∣l∣+∣v∣l+v
L s = k s I ∗ m a x ( 0 , c o s ( α ) ) p = k s I ∗ m a x ( 0 , n ⋅ h ) p L_{s}=k_{s}I*max(0, cos(\alpha))^{p}=k_{s}I*max(0, n\cdot h)^{p} Ls=ksI∗max(0,cos(α))p=ksI∗max(0,n⋅h)p
h——半程向量
Ls——高光颜色
k s k_{s} ks—— 高光反射系数
n——反光度因子
Overload中计算Blinn-Phong光照模型的shader代码如下:
/*
* BlinnPhong模型,只计算漫反射与高光
* p_LightColor: 光强
* p_LightDir:光源方向
* p_Luminosity:衰减系数
*/
vec3 BlinnPhong(vec3 p_LightDir, vec3 p_LightColor, float p_Luminosity)
{// 半程向量const vec3 halfwayDir = normalize(p_LightDir + g_ViewDir); // 计算半程向量const float diffuseCoefficient = max(dot(g_Normal, p_LightDir), 0.0); // Lambert余弦const float specularCoefficient = pow(max(dot(g_Normal, halfwayDir), 0.0), u_Shininess * 2.0);// 片元颜色:光强 * 漫反射系数 * cos(theta) * 衰减因子 + 光强 * 高光反射系数 * 高光指数 * 衰减因子return p_LightColor * g_DiffuseTexel.rgb * diffuseCoefficient * p_Luminosity + ((p_Luminosity > 0.0) ? (p_LightColor * g_SpecularTexel.rgb * specularCoefficient * p_Luminosity) : vec3(0.0));
}
二、不同光源计算
常见的光源有:平行光、点光源、聚光灯,他们的具体定义及计算可参考:https://learnopengl-cn.readthedocs.io/zh/latest/02%20Lighting/05%20Light%20casters/,里面讲的比较详细。
光源数据
不同的光源有不同的数据,而且场景中光源数量也是不确定的,所以这种情况了Overload使用OpenGL的SSBO传递数据。光源数据转换成一个矩阵,转换代码如下:
OvMaths::FMatrix4 OvRendering::Entities::Light::GenerateMatrix() const
{OvMaths::FMatrix4 result;// 存放光源位置(对应平行光存放的是方向)auto position = m_transform.GetWorldPosition();result.data[0] = position.x;result.data[1] = position.y;result.data[2] = position.z;// 光源朝向auto forward = m_transform.GetWorldForward();result.data[4] = forward.x;result.data[5] = forward.y;result.data[6] = forward.z;// 光源颜色result.data[8] = static_cast<float>(Pack(color));// 聚光灯参数result.data[12] = type;result.data[13] = cutoff;result.data[14] = outerCutoff;// 光源的衰减参数result.data[3] = constant;result.data[7] = linear;result.data[11] = quadratic;// 光源强度result.data[15] = intensity;return result;
}
Pack函数是将光颜色RGBA变成一个32位无符号整数,感兴趣可以看看,这种做法经常会见到。要想具体查看每种光源数据,可以使用RenderDoc进行查看,加深对每种光源数据的认识。RenderDoc是Shader编写利器,而且学起来也不难。

三、Overload中Standard材质的shader
Overload的材质如何创建就不再讲了,上节已经讲过的。打开一个材料例子,编辑可看到其可设置漫反射、高度、mask、法线、高光贴图,以及其他shader中使用的参数。

Shader是实现材质的核心,下面分析其代码。Standard材质的Shader在Standard.glsl文件中。
Vertex Shader
其Vertext shader代码如下:
#shader vertex
#version 430 core/*顶点着色器的入参*/
layout (location = 0) in vec3 geo_Pos; // 顶点坐标
layout (location = 1) in vec2 geo_TexCoords; // 顶点纹理坐标
layout (location = 2) in vec3 geo_Normal; // 顶点法线
layout (location = 3) in vec3 geo_Tangent; // 顶点的切线
layout (location = 4) in vec3 geo_Bitangent; // 顶点切线与法线的叉乘,三者组成一个本地坐标系/* Global information sent by the engine */
layout (std140) uniform EngineUBO
{mat4 ubo_Model; // 模型矩阵mat4 ubo_View; // 视图矩阵mat4 ubo_Projection; // 投影矩阵vec3 ubo_ViewPos; // 摄像机位置float ubo_Time;
};/* Information passed to the fragment shader */
out VS_OUT
{vec3 FragPos; // 顶点的全局坐标vec3 Normal; // 顶点法线vec2 TexCoords; // 纹理坐标mat3 TBN;flat vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;void main()
{vs_out.TBN = mat3 // 全局坐标系到本地坐标系的旋转矩阵(normalize(vec3(ubo_Model * vec4(geo_Tangent, 0.0))),normalize(vec3(ubo_Model * vec4(geo_Bitangent, 0.0))),normalize(vec3(ubo_Model * vec4(geo_Normal, 0.0))));mat3 TBNi = transpose(vs_out.TBN); // 为什么要转置?vs_out.FragPos = vec3(ubo_Model * vec4(geo_Pos, 1.0)); // 全局坐标系的下的坐标vs_out.Normal = normalize(mat3(transpose(inverse(ubo_Model))) * geo_Normal); // 全局坐标系下的法线vs_out.TexCoords = geo_TexCoords; // 纹理坐标,不用变vs_out.TangentViewPos = TBNi * ubo_ViewPos;vs_out.TangentFragPos = TBNi * vs_out.FragPos;gl_Position = ubo_Projection * ubo_View * vec4(vs_out.FragPos, 1.0);
}
其输入是顶点信息,包括顶点的坐标、法线、纹理、切线、切线与法线的叉乘。其实一般如无需特殊需求,模型只需坐标、法线、纹理即可。这里的geo_Bitangent看着像是切线与法线的叉乘,但使用RenderDoc获取顶点着色器的输入发现geo_Bitangent与切线与法线的叉乘很接近,但并不完全相等。所以geo_Bitangent究竟是不是切线与法线的叉乘不是完全肯定,但对我们看源码影响不大,暂且认为他们三个正好组成一个本地坐标系吧。
看其main函数,计算顶点全局坐标、法线、NDC坐标。注意,法线是用模型矩阵 ( M − 1 ) T (M^{-1})^{T} (M−1)T转换得到。VS_OUT中的输出量会插值,最后输给片元着色器。
片元着色器
再来看片元Shader:
#shader fragment
#version 430 core/* Global information sent by the engine */
layout (std140) uniform EngineUBO
{mat4 ubo_Model;mat4 ubo_View;mat4 ubo_Projection;vec3 ubo_ViewPos;float ubo_Time;
};/* Information passed from the fragment shader */
in VS_OUT
{vec3 FragPos;vec3 Normal;vec2 TexCoords;mat3 TBN;flat vec3 TangentViewPos;vec3 TangentFragPos;
} fs_in;/* Light information sent by the engine */
layout(std430, binding = 0) buffer LightSSBO
{mat4 ssbo_Lights[];
};/* Uniforms (Tweakable from the material editor) */
uniform vec2 u_TextureTiling = vec2(1.0, 1.0);
uniform vec2 u_TextureOffset = vec2(0.0, 0.0);
uniform vec4 u_Diffuse = vec4(1.0, 1.0, 1.0, 1.0);
uniform vec3 u_Specular = vec3(1.0, 1.0, 1.0);
uniform float u_Shininess = 100.0;
uniform float u_HeightScale = 0.0;
uniform bool u_EnableNormalMapping = false;
uniform sampler2D u_DiffuseMap;
uniform sampler2D u_SpecularMap;
uniform sampler2D u_NormalMap;
uniform sampler2D u_HeightMap;
uniform sampler2D u_MaskMap;/* Global variables */
vec3 g_Normal;
vec2 g_TexCoords;
vec3 g_ViewDir;
vec4 g_DiffuseTexel;
vec4 g_SpecularTexel;
vec4 g_HeightTexel;
vec4 g_NormalTexel;out vec4 FRAGMENT_COLOR;vec3 UnPack(float p_Target)
{return vec3(float((uint(p_Target) >> 24) & 0xff) * 0.003921568627451,float((uint(p_Target) >> 16) & 0xff) * 0.003921568627451,float((uint(p_Target) >> 8) & 0xff) * 0.003921568627451);
}bool PointInAABB(vec3 p_Point, vec3 p_AabbCenter, vec3 p_AabbHalfSize)
{return(p_Point.x > p_AabbCenter.x - p_AabbHalfSize.x && p_Point.x < p_AabbCenter.x + p_AabbHalfSize.x &&p_Point.y > p_AabbCenter.y - p_AabbHalfSize.y && p_Point.y < p_AabbCenter.y + p_AabbHalfSize.y &&p_Point.z > p_AabbCenter.z - p_AabbHalfSize.z && p_Point.z < p_AabbCenter.z + p_AabbHalfSize.z);
}vec2 ParallaxMapping(vec3 p_ViewDir)
{const vec2 parallax = p_ViewDir.xy * u_HeightScale * texture(u_HeightMap, g_TexCoords).r;return g_TexCoords - vec2(parallax.x, 1.0 - parallax.y);
}/*
* BlinnPhong模型,只计算漫反射与高光
* p_LightColor: 光强
* p_LightDir:光源方向
* p_Luminosity:衰减系数
*/
vec3 BlinnPhong(vec3 p_LightDir, vec3 p_LightColor, float p_Luminosity)
{// 半程向量const vec3 halfwayDir = normalize(p_LightDir + g_ViewDir);const float diffuseCoefficient = max(dot(g_Normal, p_LightDir), 0.0); // Lambert余弦const float specularCoefficient = pow(max(dot(g_Normal, halfwayDir), 0.0), u_Shininess * 2.0);// 片元颜色:光强 * 漫反射系数 * cos(theta) * 衰减因子 + 光强 * 高光反射系数 * 高光指数 * 衰减因子return p_LightColor * g_DiffuseTexel.rgb * diffuseCoefficient * p_Luminosity + ((p_Luminosity > 0.0) ? (p_LightColor * g_SpecularTexel.rgb * specularCoefficient * p_Luminosity) : vec3(0.0));
}// 计算衰减因子,跟LearnOpenGL中的公式一致
float LuminosityFromAttenuation(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const float constant = p_Light[0][3];const float linear = p_Light[1][3];const float quadratic = p_Light[2][3];const float distanceToLight = length(lightPosition - fs_in.FragPos);const float attenuation = (constant + linear * distanceToLight + quadratic * (distanceToLight * distanceToLight));return 1.0 / attenuation;
}vec3 CalcPointLight(mat4 p_Light)
{/* Extract light information from light mat4 */const vec3 lightPosition = p_Light[0].rgb; // 光源位置const vec3 lightColor = UnPack(p_Light[2][0]); // 光源颜色const float intensity = p_Light[3][3]; // 光强const vec3 lightDirection = normalize(lightPosition - fs_in.FragPos); // 光源方向const float luminosity = LuminosityFromAttenuation(p_Light); // 衰减因子return BlinnPhong(lightDirection, lightColor, intensity * luminosity);
}vec3 CalcDirectionalLight(mat4 light)
{return BlinnPhong(-light[1].rgb, UnPack(light[2][0]), light[3][3]);
}vec3 CalcSpotLight(mat4 p_Light)
{/* Extract light information from light mat4 */const vec3 lightPosition = p_Light[0].rgb; // 聚光灯位置const vec3 lightForward = p_Light[1].rgb; // 聚光灯朝向const vec3 lightColor = UnPack(p_Light[2][0]); // 光源颜色const float intensity = p_Light[3][3]; // 光强const float cutOff = cos(radians(p_Light[3][1])); // 内圆锥角 const float outerCutOff = cos(radians(p_Light[3][1] + p_Light[3][2])); // 内圆锥角 + 外圆锥角 const vec3 lightDirection = normalize(lightPosition - fs_in.FragPos); // 光方向const float luminosity = LuminosityFromAttenuation(p_Light); // 衰减因子/* Calculate the spot intensity */const float theta = dot(lightDirection, normalize(-lightForward)); // cos(theta)const float epsilon = cutOff - outerCutOff; // 内部圆锥角与外部圆锥角之差const float spotIntensity = clamp((theta - outerCutOff) / epsilon, 0.0, 1.0); // 边缘软化return BlinnPhong(lightDirection, lightColor, intensity * spotIntensity * luminosity);
}vec3 CalcAmbientBoxLight(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const vec3 lightColor = UnPack(p_Light[2][0]);const float intensity = p_Light[3][3];const vec3 size = vec3(p_Light[0][3], p_Light[1][3], p_Light[2][3]);return PointInAABB(fs_in.FragPos, lightPosition, size) ? g_DiffuseTexel.rgb * lightColor * intensity : vec3(0.0);
}vec3 CalcAmbientSphereLight(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const vec3 lightColor = UnPack(p_Light[2][0]);const float intensity = p_Light[3][3];const float radius = p_Light[0][3];return distance(lightPosition, fs_in.FragPos) <= radius ? g_DiffuseTexel.rgb * lightColor * intensity : vec3(0.0);
}void main()
{g_TexCoords = u_TextureOffset + vec2(mod(fs_in.TexCoords.x * u_TextureTiling.x, 1), mod(fs_in.TexCoords.y * u_TextureTiling.y, 1)); // 计算纹理贴图坐标/* Apply parallax mapping */if (u_HeightScale > 0) // 使用高度贴图g_TexCoords = ParallaxMapping(normalize(fs_in.TangentViewPos - fs_in.TangentFragPos));/* Apply color mask */if (texture(u_MaskMap, g_TexCoords).r != 0.0) // 可以通过u_MaskMap屏蔽部分区域{g_ViewDir = normalize(ubo_ViewPos - fs_in.FragPos); // 视线方向(视点坐标-片元坐标)g_DiffuseTexel = texture(u_DiffuseMap, g_TexCoords) * u_Diffuse; // 漫反射颜色g_SpecularTexel = texture(u_SpecularMap, g_TexCoords) * vec4(u_Specular, 1.0); // 高光项的颜色if (u_EnableNormalMapping) // 使用法线贴图{g_Normal = texture(u_NormalMap, g_TexCoords).rgb;g_Normal = normalize(g_Normal * 2.0 - 1.0); g_Normal = normalize(fs_in.TBN * g_Normal);}else{g_Normal = normalize(fs_in.Normal);}vec3 lightSum = vec3(0.0);// 对灯光进行循环,计算每盏灯的贡献for (int i = 0; i < ssbo_Lights.length(); ++i){switch(int(ssbo_Lights[i][3][0])){case 0: lightSum += CalcPointLight(ssbo_Lights[i]); break; // 计算点光源case 1: lightSum += CalcDirectionalLight(ssbo_Lights[i]); break; // 计算case 2: lightSum += CalcSpotLight(ssbo_Lights[i]); break; // 计算聚光灯case 3: lightSum += CalcAmbientBoxLight(ssbo_Lights[i]); break;case 4: lightSum += CalcAmbientSphereLight(ssbo_Lights[i]); break;}}FRAGMENT_COLOR = vec4(lightSum, g_DiffuseTexel.a);}else{FRAGMENT_COLOR = vec4(0.0);}
}
Fragment Sahder代码看着很多,拆解一下就是分别计算各个灯光的贡献,进行累加。计算每种灯光时,最终都是使用Blinn-Phonge模型计算的。每种类型的灯光基本与LearnOpenGL中的描述一致。UnPack函数可以学习一下,看看如何float如何变成RGB。
相关文章:
【Overload游戏引擎细节分析】standard材质Shader
提示:Shader属于GPU编程,难写难调试,阅读本文需有一定的OpenGL基础,可以写简单的Shader,不适合不会OpenGL的朋友 一、Blinn-Phong光照模型 Blinn-Phong光照模型,又称为Blinn-phong反射模型(Bli…...
Leetcode—7.整数反转【中等】
2023每日刷题(十) Leetcode—7.整数反转 关于为什么要设long变量 参考自这篇博客 long可以表示-2147483648而且只占4个字节,所以能满足题目要求 复杂逻辑版实现代码 int reverse(int x){int arr[32] {0};long y;int flag 1;if(x <…...
lua-web-utils和proxy设置示例
以下是一个使用lua-web-utils和proxy的下载器程序: -- 首先安装lua-web-utils库 local lwu require "lwu" -- 获取服务器 local function get_proxy()local proxy_url "duoipget_proxy"local resp, code, headers, err lwu.fetch(proxy_…...
分享一下在微信小程序里怎么添加储值卡功能
在微信小程序中添加储值卡功能,可以让消费者更加便捷地管理和使用储值卡,同时也能增加商家的销售收入。下面是一篇关于如何在微信小程序中添加储值卡功能的软文。 标题:微信小程序添加储值卡功能,便捷与高效并存 随着科技的不断发…...
2023高频前端面试题-http
1. HTTP有哪些⽅法? HTTP 1.0 标准中,定义了3种请求⽅法:GET、POST、HEAD HTTP 1.1 标准中,新增了请求⽅法:PUT、PATCH、DELETE、OPTIONS、TRACE、CONNECT 2. 各个HTTP方法的具体作用是什么? 方法功能G…...
图像识别在自动驾驶汽车中的多传感器融合技术
摘要: 介绍文章的主要观点和发现。 引言: 自动驾驶汽车的兴起和重要性。多传感器融合技术在自动驾驶中的关键作用。 第一部分:图像识别技术 图像识别的基本原理。图像传感器和摄像头在自动驾驶中的应用。深度学习和卷积神经网络ÿ…...
Kafka To HBase To Hive
目录 1.在HBase中创建表 2.写入API 2.1普通模式写入hbase(逐条写入) 2.2普通模式写入hbase(buffer写入) 2.3设计模式写入hbase(buffer写入) 3.HBase表映射至Hive中 1.在HBase中创建表 hbase(main):00…...
python pandas.DataFrame 直接写入Clickhouse
import pandas as pd import sqlalchemy from clickhouse_sqlalchemy import Table, engines from sqlalchemy import create_engine, MetaData, Column import urllib.parsehost 1.1.1.1 user default password default db test port 8123 # http连接端口 engine create…...
德语中第二虚拟式在主动态的形式,柯桥哪里可以学德语
德语中第二虚拟式在主动态的形式 1. 对于大多数的动词,一般使用这样的一般现在时时态: wrde 动词原形 例句:Wenn es nicht so viel kosten wrde, wrde ich mir ein Haus am Meer kaufen. 如果不花这么多钱,我会在海边买一栋房…...
[Python进阶] 消息框、弹窗:tkinter库
6.16 消息框、弹窗:tkinter 6.16.1 前言 应用程序中的提示信息处理程序是非常重要的部分,用户要知道他输入的资料到底正不正确,或者是应用程序有一些提示信息要告诉用户,都必须通过提示信息处理程序来显示适当的信息,…...
(免费领源码)java#Springboot#mysql装修选购网站99192-计算机毕业设计项目选题推荐
摘 要 随着科学技术,计算机迅速的发展。在如今的社会中,市场上涌现出越来越多的新型的产品,人们有了不同种类的选择拥有产品的方式,而电子商务就是随着人们的需求和网络的发展涌动出的产物,电子商务网站是建立在企业与…...
生活废品回收系统 JAVA语言设计和实现
目录 一、系统介绍 二、系统下载 三、系统截图 一、系统介绍 基于VueSpringBootMySQL的生活废品回收系统包含资源类型模块、资源品类模块、回收机构模块、回收机构模块、资源销售单模块、资源交易单模块、资源交易单模块,还包含系统自带的用户管理、部门管理、角…...
redhat/centos 配置本地yum源
- 详细步骤(首先需要将iso文件上传到服务器): 1. mkdir /media/cdrom #新建镜像文件挂载目录2. cd /usr/local/src #进入系统镜像文件存放目录3. ls #列出目录文件,可以看到刚刚上传的系统镜像文件4. mount -t iso9660 -o loop /usr/local/src/rhel-s…...
FLStudio2024汉化破解版在哪可以下载?
水果音乐制作软件FLStudio是一款功能强大的音乐创作软件,全名:Fruity Loops Studio。水果音乐制作软件FLStudio内含教程、软件、素材,是一个完整的软件音乐制作环境或数字音频工作站... FL Studio21简称FL 21,全称 Fruity Loops Studio 21,因此国人习惯叫…...
Java 音频处理,音频流转音频文件,获取音频播放时长
1.背景 最近对接了一款智能手表,手环,可以应用与老人与儿童监控,环卫工人监控,农场畜牧业监控,宠物监控等,其中用到了音频传输,通过平台下发语音包,发送远程命令录制当前设备音频并…...
Spring Boot发送邮件
在现代的互联网应用中,发送电子邮件是一项常见的功能需求。Spring Boot提供了简单且强大的邮件发送功能,使得在应用中集成邮件发送变得非常容易。本文将介绍如何在Spring Boot中发送电子邮件,并提供一个完整的示例。 1. 准备工作 在开始之前…...
智慧矿山:AI算法助力!刮板机监测,生产效率和安全性提升!
工作面刮板机在煤矿等采矿场景中起着重要作用。为了提高其生产效率和安全性,研究人员开发了一种基于 AI 算法的刮板机监测技术。 在传统的刮板机监测中,通常需要人工观察和判断刮板机的状态。这种方法存在许多问题,如主观性、耗时和易出错等。…...
Qt跨平台(统信UOS)各种坑解决办法
记录Qt跨平台的坑,方便日后翻阅。 一、环境安装 本人用的是qt 5.14.2.直接在官网下载即可。地址:Index of /archive/qt/5.14/5.14.2 下载linux版本。 下载之后 添加可执行权限。 chmod 777 qt-opensource-linux-x64-5.14.2.run 然后执行。 出现坑1…...
ORB-SLAM3算法1之Ubuntu18.04+ROS-melodic安装ORB-SLAM3及各种问题解决
文章目录 0 引言1 安装依赖1.1 opencv安装1.2 Eigen3安装1.3 Pangolin安装1.4 其他2 编译安装ORB-SLAM32.1 build.sh2.2 build_ros.sh0 引言 ORB-SLAM3,在之前ORB-SLAM和ORB-SLAM2的基础上,新增了IMU多传感器融合SLAM,这是第一个能够使用针孔和鱼眼镜头模型通过单目、立体和…...
git学习笔记之用命令行解决冲突
背景 一般来说,当使用git检测到源分支和目标分支发生冲突时,我们习惯用IDE在本地进行冲突的解决,再合并、push。 但如果冲突文件不多,我们大可以直接用命令行去解决冲突。 方法 第一种方法: 找到所有的>>>…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
