移动平台实时动态多点光源方案:Cluster Light
一、什么是 Cluster Light,它具体如何实现多点光源效果?
对于移动设备,如何支持场景中大量的实时点光源一直以来都是比较棘手的问题,因此对于过去,往往有如下两种常规方案:
- 静态点光源直接烘焙,光源本身依靠自发光 + Bloom 出效果
- 动态点光源标记最重要的 1~4 盏,shader 中只计算这些标记为 Important 的点光源的贡献
但如果场景中有大量的点光源,又或者说点光源的数量、位置无法预知,那么前两种方案就会完全不可行,除此之外对于方案②,场景中不同位置的点光源也很难分辨出哪个是 “Important” 的,毕竟这个标签若要跟着场景走,你很难说哪个光源重要或者不重要
因此,基于空间划分后分块计算光照的思路就应运而生,其可以在保证效果的同时减少 PixelShader 的计算量,大体思路也很简单,看一张图就多多少少有所理解
有兴趣可以翻看当年的 PPT,这里直接上重点,一个简单的多点光源 ClusterLight 思路如下:
- ViewSpace,即摄像机可见区域,分块(GPU ComputeShader)
- 计算每个块(Cluster)会受到哪些点光源影响
- 在着色时,根据像素获取对应的 Cluster,并拿到光照列表
- 正常计算点光源着色
1.1 简单 ClusterLight 实现
可参考链接:这些都是知乎上个人实现的 ClusterLight Demo,除此之外,URP12 之后的版本也支持 PC 下的 ClusterLight,大部分情况,ClusterLight 方案都不包含阴影,关于阴影的问题后面也会提到
- Unity SRP学习笔记(二):Cluster Based Light Culling
- Unity SRP 实战(四)Cluster Based Lighting
- 实时动态多光源渲染-Cluster Forward Shading
- Cluster Based Deferred Lighting-MaxwellGeng
1.1.1 第一步:按照 ViewSpace 切割 Cluster
摄像机的可见区域,即摄像机的视锥体,按照 X, Y, Z 等距切割成多个 Cluster,这里没有复杂的数学公式,加上其没有前后依赖计算,因此可以并行,交给 ComputeShader 非常的合适
X, Y 轴的切割没啥好说的,直接等距切割就 OK,传统的 Frustum Light 方式 Z 方向不切割,Frustum 截锥体如下图,如果继续按 Z 轴切割就是本文的 Cluster 了:
考虑到离摄像机越远的位置,相同距离的 对于屏幕 pixel 的贡献就可能越小,因此 Z 轴可以按照距离指数分割,即离摄像机越远,Z 方向上 Cluster 分的块就越大,反之越小,不过这个对于 Deferred Lighting,或是在屏幕空间进行的光照计算优化明显,目前测试下来 Forward 物体着色时计算光照优化不明显,因此依然可以仅等距划分 Z
1.1.2 第二步:收集光照信息,再次计算每个 Cluster 包含哪些光源
到此 GPU 需要获取两个 StructuredBuffer:一个是 ClusterBox 对应的 List:每个 Cluster 数据包含八个 Vector3,对应锥体的每个顶点
另一个则是场景中所有点光源列表:
protected struct PointLight
{public Vector4 color;public Vector4 position;
};
其中 Color 的第四维为光源强度,position 第四维存储光源半径
一样可以并行计算:对于每个 Cluster 做一个几何判断,即当前 Cluster 与球体是否相交,计算方案有很多,这里提供两个正确的经典思路:
一是求出 Cluster 对应的 AABB 方形包围盒,之后判断这个包围盒是否与球体相交:这个方案相对后者计算量没那么大,缺点就是可能会有浪费,可能 Cluster 不会受到某个光源影响,但仍然会统计这个光源
bool TestSphereVsAABB(float4 s, AABB aabb)
{float3 center = (aabb.max1.xyz + aabb.min1.xyz) * 0.5f;float3 extents = (aabb.max1.xyz - aabb.min1.xyz) * 0.5f;float3 vDelta = max(0, abs(center - s.xyz) - extents);float fDistSq = dot(vDelta, vDelta);return fDistSq <= s.w * s.w;
}
二是对于 Cluster 的每个面判断面交,6个面计算六次,优点就是精准,但是要额外考虑光源球体完全在 Cluster 内部的情况,因此还要计算下空间相对位置,比较麻烦
同样,这一部分也交给 GPU Compute 计算,最后可以得到一张光源分配查找表,即每个 Cluster,对应一个 LightList,即该 Cluster 会接收到的点光源的列表,不过考虑到每个列表的大小必然不会一样,GPU 没法申请 动态大小的 List,因此为了避免空间浪费,可以多加一个 LightIndexList 用于存储光源索引,此时光源查找表可以只记录每个 Cluster 对应的 LightIndexList 的起点和数量,最后通过 LightIndexList 查询对应连续的一段内存来获取 LightList 的实际索引:
[numthreads(32, 32, 1)]
void LightAssign(uint3 tid : SV_GroupThreadID, uint3 id : SV_GroupID)
{// cluster ID uint i = tid.x, j = tid.y, k = id.x;uint3 clusterId_3D = uint3(i, j, k);uint clusterId_1D = Index3DTo1D(clusterId_3D);ClusterBox box = _clusterBuffer[clusterId_1D];uint startIndex = clusterId_1D * _maxNumLightsPerCluster;uint endIndex = startIndex;for(int lid = 0; lid < _numLights; lid++){PointLight pl = _lightBuffer[lid];if(!ClusterLightIntersect(box, pl))continue;_lightAssignBuffer[endIndex++] = uint(lid);}LightIndex idx;idx.count = endIndex - startIndex;idx.start = startIndex;_assignTable[clusterId_1D] = idx;
}
需要注意的是,这个数据量还是不小的,假设 ViewSpace X, Y, Z 分别按照 32, 32, 64 的大小切割,限制每个 Cluster 最多受到 8 盏点光源影响,这样就需要至少 32 * 32 * 64 * 8 = 524288 的 lightAssignID 大小
后续会介绍这一部分怎么优化,或者是否有其它的存储方式
1.1.3 第三步:光照计算
这一部分就比较简单了,着色时判断当前 pixel 在哪个 cluster 中,获取其光照索引表,拿到其光照之后就是标准的遍历点光源计算光照,出于性能考虑的话可以直接用最简单的兰伯特光照模型
#if defined(VIEW_CLUSTER_LIGHT)float2 scrPos = i.scrPos.xy / i.scrPos.w;float depth = LinearEyeDepth(i.pos.z, _ZBufferParams);float A = LinearEyeDepth(0, _ZBufferParams);float B = LinearEyeDepth(1, _ZBufferParams);// 计算 Cluster Based Lightinguint x = floor(scrPos.x * _numClusterX);uint y = floor(scrPos.y * _numClusterY);#if UNITY_REVERSED_Zuint z = (_numClusterZ - 1) - floor(((depth - B) / A) * _numClusterZ);#elseuint z = floor(((depth - A) / B) * _numClusterZ);#endif
#endif
不过考虑到 DirectX 和 GL 的平台差异,还要处理一下 Reserve-Z 问题,在计算 Cluster 的 Z 索引时,对于 Reversed-Z 的平台要把 Z 部分的计算反过来那分割数量减去它,这和你 ComputeShader 中的计算逻辑也有一定关系,在 ComputerShader 中考虑好 Reserve-Z 应该也是可行的
光照计算部分代码就不贴了,可以根据实际情况选择使用任意光照模型
还需要注意的是,点光源的阴影计算并不包含其中,依旧需要额外处理阴影的问题,并且对于 foward 管线这部分没有高性能的方案,一个简单地思路就是对于每个点光源求出其 CubeShadowmap,对于多个点光源可以得到一个 TexCubeArray,着色时通过 index 读取采样 shadowmap
1.2 世界空间 ClusterLight 分割
前面介绍的就是经典的 ViewSpace 分割方案,但是,技术一定是要依赖需求去动态调整的,生搬硬套没有意义,考虑到大多数手机游戏,点光源往往都是静态烘焙的做法,根本没有必要上动态的多光源
而需要动态点光源的,可能是一些特殊的场景,又或者是点光源位置不能确定的场景,例如家园,玩家可以任意摆放建筑和摆饰,而部分摆饰会有光源,又或者说是空间小室内场景。对于这些需求的特点就是:我们没有必要将点光源和大世界绑定在一起,对于如上情况而言,可以布置或者说会出现动态点光源的空间是有限的,此时按照世界空间分 Cluster 就成了一种可行的选择,并且相对于 ViewSpace 的分块,后者无需在摄像机改变视角时实时更新,性能也会更好
既然是世界空间的分割,那么分割范围大小就要有严格限制:可以通过放置 Box 来确定其光源生效范围,后续只对这个 Box 内的空间进行分割及光照计算
而后续计算 Cluster 对应光照数据的流程,和前者 View-Space 的计算流程没有差异,甚至可以共用一个 ComputeShader,最后在着色时,也无须考虑平台差异
#if defined(VIEW_CLUSTER_LIGHT)…… ViewSpace Cluster
#elsefloat3 dir = maxBound - minBound;float X = (worldPos.x - minBound.x) / dir.x;float Y = (worldPos.y - minBound.y) / dir.y;float Z = (worldPos.z - minBound.z) / dir.z;uint x = floor(X * _numClusterX);uint z = floor(Y * _numClusterZ);uint y = floor(Z * _numClusterY);
#endif
二、移动平台性能优化及兼容
很可惜,如果是无脑上 ClusterLight 的话,无论是自己的方案,或者是 URP UE 自带的方案,在大部分中配机型上都很难跑的动,甚至是不兼容,抛开阴影不谈,经过测试原因主要如下:
- 部分手机理论应该支持 ComputeShader,但是仍旧会出现进游戏闪退等问题
- 对于①深度了解主要又分两种情况:手机驱动尽管是 OpenGL3.1 以上版本,但仍不支持
- 支持 ComputeShader,但是不支持 StructuredBuffer(SSBO)
- 可以进入游戏,但是有很明显的掉帧,主要原因出在 StructuredBuffer pixelShader 访问上
2.1 StructuredBuffer pixelShader 优化
StructuredBuffer 测试报告.md
对于 ClusterLight 的数据,往往都通过 StructuredBuffer 存储,这样是非常常见的操作:
RWStructuredBuffer<ClusterBox> _clusterBuffer;
RWStructuredBuffer<PointLight> _lightBuffer;
RWTexture3D<float4> _assignTable;
但是在 pixelShader 中,读取 StructuredBuffer 对于部分手机而言会出现非常离谱的帧数下降:
以 Mi6(骁龙835为例),正常使用 StructuredBuffer 未优化的性能如下:
整体稳定在 30FPS,但是运行一段时间后手机会降频(735MHz - 515MHz),此时无法稳定 30FPS
之前存储灯光是使用 StructuredBuffer 的,一般情况下场景中的灯光都会有最大数量限制,如果最大数量限制 128 个,那么实际操作上就可以使用一个长度为 64 的 Matrix[] 来存储至多 128 盏光源信息
Matrix[64] 必然可以定义在 ConstantBuffer 中而非 StructuredBuffer,前者读写性能远好于后者
float4x4 _lightBuffer[64];
float4x4 lit = _lightBuffer[lightId / 2]; // 根据 id 查灯光表
float4 litPosition = lit[(lightId % 2) * 2 + 1];
float4 litColor = lit[(lightId % 2) * 2];
在仅做了这步优化后,实机测试性能就有了肉眼可见的提升:
可以看到,原先 30FPS 提升到了 50FPS,这也证明了这个优化路线是正确的
关于 StructuredBuffer 在移动设备上出现严重性能下降的原因推测:
当前设备并不能很好的支持在 pixel 中访问 StructuredBuffer 或者本质上不支持 StructuredBuffer,因此在使用 StructuredBuffer 时,为了避免更坏情况(直接闪退 or 不执行),对应的逻辑会退化,从而在读写上出现了不可预料的时间消耗
2.1.1 使用 Texture3D
除此之外,每个 ClusterBox 存储光源信息,也可不使用 StructuredBuffer 而是用 Texture3D 替代,其 Texture3D 的每个 pixel 正好和切割后的每个 Cluster 一一对应
private int numClusterX = 32;
private int numClusterY = 32;
private int numClusterZ = 32;
public JClusterCPUGenerate(int Z, BoxCollider collider)
{numClusterZ = Z;worldBox = collider.bounds;assignTable = new Texture3D(numClusterX, numClusterY, numClusterZ, TextureFormat.RGBAFloat, true);Color[] colors = new Color[numClusterX * numClusterY * numClusterZ];for (int i = 0; i < colors.Length; i++){colors[i] = Color.black;}assignTable.SetPixels(colors);
}
Texture3D 只能支持4通道,也就是最大 ARGBFloat,这就意味着没有特殊操作的话每个 Cluster 只能存储最多4盏灯,这样的话还是需要像前面 1.1.2 的操作一样,这里只存储两个 Int 数据,对应 lightBuffer 的起始 Index 和灯光数量,在着色计算时最终从 LightBuffer 里获取灯光,这样就没有灯光数量限制了
此时 Texture3D 也可以使用 RGInt 格式足够,使用 Texture3D 而非 StructuredBuffer 性能可以得到进一步提升:
可以看到,目前设备已经能够稳定 60FPS,尽管运行一段时间后手机仍会降频
2.2.2 灯光数据位存储
前面提到过:Texture3D 只能支持4通道,也就是最大 ARGBFloat,这就意味着没有特殊操作的话每个 Cluster 只能存储最多4盏灯,但是真的只能存储4盏灯嘛?考虑到场景中的灯光数量必然有一个上限,以128的上限为例,其灯光 Index 必然是在 0~127 的范围内,此时对于 RGBAFloat 或 RGBAInt 格式,一个通道就可以存储4盏灯光
例如一个 Cluster 受到第2,4,16,36这四盏灯光影响,那么其第一个通道的数据存储值就为:
-
00000001 00000010 00001000 00100100 = 526337
在实际获取数据时,再通过位运算就可以解算其灯光索引,考虑到位运算效率很高,因此不会带来太大的性能问题,除此之外,一张 Texture3D 单通道也可以存储至多16盏灯光
如果设置了16盏灯光为单个 pixel 可接受的点光源数量上限,那么就无需在申请 LightBuffer 或 lightIndex 这样的额外的 StructuredBuffer 了
[numthreads(32, 32, 1)]
void LightAssign(uint3 tid : SV_GroupThreadID, uint3 id : SV_GroupID)
{// cluster ID uint i = tid.x, j = tid.y, k = id.x;uint3 clusterId_3D = uint3(i, j, k);uint clusterId_1D = Index3DTo1D(clusterId_3D);ClusterBox box = _clusterBuffer[clusterId_1D];uint startIndex = 0, endIndex = 0;int lightAssignID[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};for(int lid = 0; lid < _numLights && endIndex < 16; lid++){PointLight pl = _lightBuffer[lid];if(!ClusterLightIntersect(box, pl))continue;lightAssignID[endIndex++] = int(lid) + 1;}int A = (lightAssignID[0] << 16) + lightAssignID[1];int B = (lightAssignID[2] << 16) + lightAssignID[3];int C = (lightAssignID[4] << 16) + lightAssignID[5];int D = (lightAssignID[6] << 16) + lightAssignID[7];_assignTable[clusterId_3D] = float4(A, B, C, D);
}
以上代码为16位存储,即每个 int 存储两盏灯光,此时最多支持的单个 pixel 灯光上限为8盏,事实上,手机上也不太好布置太密的点光源,一个 pixel 接受8盏光源已足矣,否则计算量其实也优化不下来
到此就成功完全弃用了 StructuredBuffer,以避免其读写慢带来的致命性能损耗:
稳定 60FPS 的同时,测试机在一段时间内也没有降频现象
2.2 CPU 实现 ClusterLight
ComputeShader 手机兼容性报告
很可惜,并非所有手机都能很好的支持 ComputeShader,在引用中所有测试的 Android 手机中,OpenGL ES 3.1以上的手机均使用的是 Shader Model 4.5 或 Shader Model 5.0,在使用 unity 提供的 API:SystemInfo.supportsComputeShaders 在上述手机显示为 true,但通过运行一段 ComputeShader 程序,在 Shader Model4.5 上运行结果却不符合预期
因此,如果想要不兼容 CS 的手机也能实现 ClusterLight 动态点光源,就需要考虑备选方案,也就是使用 CPU 计算原先 CS 计算的部分
不过如果这部分任务交给 CPU,CPU 要不考虑使用 JobSystem 来并行计算,要不就考虑分帧,不然如此大的计算量,尽管 WorldSpace 的 ClusterLight 仅需一次计算,但仍然会出现卡帧问题
下面给出一个分帧的实现:分帧分什么?当然是光源 —— 即每帧只处理一盏光源
思路也很简单,对于每个当前 Add 的光源,以其光源为中心开启 BFS,搜索所有受到该光源影响到的 Cluster,此时复杂度为线性:
private void BFSCluster(PointLight light, bool isAdd)
{int clusterNum = numClusterY * numClusterZ;Queue<Cluster> queue = new Queue<Cluster>();uint[] flag = new uint[clusterNum];Vector3 pos = new Vector3(light.position.x, light.position.y, light.position.z);Cluster lightCluster = Locate(light, pos);queue.Enqueue(lightCluster);flag[lightCluster.y * numClusterY + lightCluster.z] |= (uint)1 << lightCluster.x;Vector3 perCusterlen = new Vector3((worldBox.max.x - worldBox.min.x) / numClusterX, (worldBox.max.y - worldBox.min.y) / numClusterZ,(worldBox.max.z - worldBox.min.z) / numClusterY);while (queue.Count != 0){Cluster s = queue.Dequeue();ChangeLightState(s, light, isAdd);int[,] dirS = { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 } };for (int i = 0; i < 6; i++){Cluster n = s;n.x += dirS[i, 0];n.y += dirS[i, 1];n.z += dirS[i, 2];if (n.x >= numClusterX || n.y >= numClusterZ || n.z >= numClusterY || n.x < 0 || n.y < 0 || n.z < 0)continue;if ((flag[n.y * numClusterY + n.z] & (uint)1 << n.x) != 0)continue;if (n.deep++ > 0){if (!CheckLightDir(n, lightCluster, perCusterlen, light.position.w))continue;}queue.Enqueue(n);flag[n.y * numClusterY + n.z] |= (uint)1 << n.x;}}
}
2.2.1 基于 BFS 的光源 Add 方案
关于 BFS(代码中为队列实现的广度优先搜索),默认看到这里的人都是了解的,因此不会介绍这部分算法,重点提一下搜索时的标记数组 Flag[],因为该 BFS 为避免重复搜索采用的是记忆化搜索的思路
Flag[] 的大小理论上应该和 Cluster 的数量一致,但是如果每帧都申请并清空这样大小的 Flag[],必然也会浪费资源,因为 Cluster 的数量可能会过万,因此这部分需要做点处理:
- 清空标记处理:如果是分帧操作,则没必要每帧都重新 new 一个数组,也没必要像 memset 一样重置 flag[] 的值,可以直接给 flag[] 填上当前的灯光 ID 作为标记
- 此方案和①不兼容,不过可以省掉大量的 flag[] 空间,一样是利用位运算,申请 flag 时只需要申请 X Y 两轴的大小,Z 存储到对应的位中,不过此方案也要求 Z 不能超过位数(uint=32),上面的代码样例采用的就是这个方案
相关文章:
移动平台实时动态多点光源方案:Cluster Light
一、什么是 Cluster Light,它具体如何实现多点光源效果? 对于移动设备,如何支持场景中大量的实时点光源一直以来都是比较棘手的问题,因此对于过去,往往有如下两种常规方案: 静态点光源直接烘焙࿰…...
2024年03月CCF-GESP编程能力等级认证C++编程八级真题解析
本文收录于专栏《C++等级认证CCF-GESP真题解析》,专栏总目录:点这里。订阅后可阅读专栏内所有文章。 一、单选题(每题 2 分,共 30 分) 第 1 题 为丰富食堂菜谱,炒菜部进行头脑风暴。肉类有鸡肉、牛肉、羊肉、猪肉4种,切法有肉排、肉块、肉末3种,配菜有圆白菜、油菜、…...
(十一)图像的罗伯特梯度锐化
环境:Windows10专业版 IDEA2021.2.3 jdk11.0.1 OpenCV-460.jar 系列文章: (一)PythonGDAL实现BSQ,BIP,BIL格式的相互转换 (二)BSQ,BIL,BIP存储格式的相互转换算法 (三…...
实验九 枚举问题(运算模拟)
实验名称:实验九 枚举问题(运算模拟) 实验目的:熟练掌握一些枚举问题的处理方法。 实验内容: 问题描述:(乘积为n个1的数字游戏)两位计算机爱好者在进行“积为n个1的数字游戏”&a…...
2024 年 AI 辅助研发趋势:从研发数字化到 AI + 开发工具 2.0,不止于 Copilot
1. 背景介绍 随着人工智能技术的飞速发展,AI在软件开发领域的应用越来越广泛。从最初的代码补全、错误提示,到现在的代码生成、自动化测试,AI正在逐步改变软件开发的模式。2024年,AI辅助研发的趋势已经从研发数字化向AI开发工具2…...
UE5数字孪生系列笔记(三)
C创建Pawn类玩家 创建一个GameMode蓝图用来加载我们自定义的游戏Mode新建一个Pawn的C,MyCharacter类作为玩家,新建一个相机组件与相机臂组件,box组件作为根组件 // Fill out your copyright notice in the Description page of Project Set…...
ASR-LLM-TTS 大模型对话实现案例;语音识别、大模型对话、声音生成
参考:https://blog.csdn.net/weixin_42357472/article/details/136305123(llm+tts) https://blog.csdn.net/weixin_42357472/article/details/136411769 (asr+vad) 这里LLM用的是chatglm;电脑声音播报用的playsound 1、实时语音识别版本 注意:暂时这项目有个缺陷就是tts…...
主干网络篇 | YOLOv8更换主干网络之EfficientNet
前言:Hello大家好,我是小哥谈。EfficientNet是一种高效的卷积神经网络架构,由Mingxing Tan和Quoc V. Le在2019年提出,其设计思想是在不增加计算复杂度的情况下提高模型的准确性。它引入了一个称为"复合系数"的概念,该系数用于同时缩放网络的深度、宽度和分辨率。…...
Web开发-Django学习笔记
客户端如何获取服务端的数据信息? 通常 是 HTTP网络协议,通过网络传输数据信息。 客户端通过HTTP协议发送请求信息给服务端,并从服务端接收响应信息。 Web 前端开发: (HTML、CSS、JS)文件部署在后端服务…...
关于深度学习的 PyTorch 项目如何上手分析?从什么地方切入?
文章目录 PyTorch 项目分析1.背景2.分析流程 PyTorch 项目分析 1.背景 当我们拿到一个 PyTorch 的深度学习项目时,应该怎么入手?怎么去查看代码? 2.分析流程 首先阅读对应项目的 README.md 文件。通过阅读 README.md ,一般可以…...
JavaEE企业开发新技术4
2.16 模拟Spring IOC容器功能-1 2.17 模拟Spring IOC容器功能-2 什么是IOC? 控制反转,把对象创建和对象之间的调用过程交给Spring框架进行管理使用IOC的目的:为了耦合度降低 解释: 模仿 IOC容器的功能,我们利用 Map…...
CSS使用JS变量
1. CSS变量 CSS 变量(也称为自定义属性)允许我们在 CSS 中定义可重复使用的值,并将其应用于不同的选择器。为了创建一个 CSS 变量,我们需要使用 -- 前缀,然后可以像常规属性一样使用它。 :root {--primary-color: bl…...
拆分巨石:将MVPS和MVAS应用于遗留应用程序——可持续架构(六)
前言 MVP 和 MVA 的概念不仅适用于新应用程序;它们提供了一种新颖的方式来审视对遗留系统的范围变更,以防止过快地承担过多的变化 - 参见图1。MVA 可以帮助组织评估和更新其技术标准,通过展示新技术如何真正对支持 MVP 至关重要。创建 MVA 可…...
Linux renice命令教程:如何优雅地调整进程优先级(附案例详解和注意事项)
Linux renice命令介绍 renice命令在Linux中用于修改已经运行的进程的优先级。这个命令允许你改变一个已经运行的进程的调度优先级。如果我们给一个进程设置了更高的优先级,那么内核将为该进程分配更多的CPU时间。 Linux renice命令适用的Linux版本 renice命令在所…...
Gitea 的详细介绍
什么是 Gitea? Gitea 是一个开源、轻量级的自托管 Git 服务,它允许用户搭建类似于 GitHub 或 GitLab 的代码托管平台。由于采用 Go 语言开发,Gitea 具有高效的性能和跨平台特性,适合个人开发者或小团队使用。 Gitea 的特点 轻量…...
Kotlin object
object 的三种用法 Kotlin 的 object 关键字有三种用法: 对象声明 ,一般用来实现单例伴生对象 ,类似 Java 的 static 关键字,也可以用于工厂方法模式对象表达式 ,一般用来代替 Java 的匿名内部类 对象声明 object 的语义是这样的: 定义一个类并创建一个实例 。不管是对象…...
【Redis】数据类型、事务执行、内存淘汰策略
目录 数据类型 Redis事务执行步骤 步骤: redis内存淘汰策略 设置内存淘汰策略 1.设置配置文件 2.通过命令设置 数据类型 官网解释 Understand Redis data types | Redis 首先,Redis 的所有键都是字符串,常用的数据类型有 5 种:Strin…...
Python Flask Web框架初步入门
前言 flask基础 搭建flask服务器 定义html 使用templates模板定义页面的html html页面编写 render_template传参变量 定义图片 创建static目录,存入图片 html编写 flask入门 网站多域名 网站之间超链接跳转 入门案例 将centos的rpm包下载链接集成到自…...
【设计模式】工厂方法模式详解
在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们…...
独立游戏《星尘异变》UE5 C++程序开发日志3——UEC++特供的数据类型
本篇日志将介绍FString,FText、FName的用法和相互转换,以及容器TMap,TArray的增删查改 一、字符串相关数据类型:FString、FText、FName FString是最接近std::string的类型,字符串本身可以看做一个存储char型的动态数…...
递归方法的理解
递归方法调用 :方法自己调用自己的现象就称为递归。 递归的分类 : 直接递归、间接递归。 直接递归:方法自身调用自己 public void methodA (){ methodA (); } 间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法&am…...
css之flex布局文本不换行不显示省略号的解决方法
文章目录 一、单行长文本显示省略号二、flex布局下的处理技巧 一、单行长文本显示省略号 先讲讲常规情况下长文本不跨行显示省略号的代码: overflow: hidden; //不允许内容超出盒子 white-space: nowrap; //不允许文本跨行 text-overflow: ellipsis; //文本超…...
华清远见STM32U5开发板助力2024嵌入式大赛ST赛道智能可穿戴设备及IOT选题项目开发
第七届(2024)全国大学生嵌入式芯片与系统设计竞赛(以下简称“大赛”)已经拉开帷幕,大赛的报名热潮正席卷而来,高校电子电气类相关专业(电子、信息、计算机、自动化、电气、仪科等)全…...
若依框架实现不同端用户登录(后台管理用户和前台会员登录——sping security多用户)
目录 需求背景 前期准备 实现UserDetailsService接口 改造loginUser 声明自定义AuthenticationManager 的bean 自定义登录接口 参考文章 效果如下 需求背景 用若依搭建的后台管理环境,但是前台用户系统(前端)并没有和若依的前端集成在一起。…...
【解決|三方工具】Obi Rope 编辑器运行即崩溃问题
开发平台:Unity 2021.3.7 三方工具:Unity资产工具 - Obi Rope 问题背景 使用Unity三方开发工具 - Obi Rope 模拟绳索效果。配置后运行 Unity 出现报错并崩溃。通过崩溃日志反馈得到如下图所示 这是一个序列化问题造成的崩溃,指向性为 Obi…...
岭师大数据技术原理与应用-序章-软工版
HeZaoCha-CSDN博客 序章—软工版 一、环境介绍1. VMware Workstation Pro2. CentOS3. Java4. Hadoop5. HBase6. MySQL7. Hive 二、系统安装1. 虚拟网络编辑器2. 操作系统安装 三、结尾 先说说哥们写这系列博客的原因,本来学完咱也没想着再管部署这部分问题的说&…...
Leetcode 680. 验证回文串 II
给你一个字符串 s,最多 可以从中删除一个字符。 请你判断 s 是否能成为回文字符串:如果能,返回 true ;否则,返回 false 。 示例 1: 输入:s “aba” 输出:true 示例 2:…...
网络安全接入认证-802.1X接入说明
介绍 802.1X是一个网络访问控制协议,它可以通过认证和授权来控制网络访问。它的基本原理是在网络交换机和认证服务器之间建立一个安全的通道,并要求客户端提供身份验证凭据。如果客户端提供的凭据是有效的,交换机将开启端口并允许访问。否则&…...
iPhone的iOS系统:定义移动智能体验,引领科技潮流之巅
来自:dlshuhua.com/post/83721.html 在移动智能设备领域,iPhone一直以其出色的性能和独特的用户体验脱颖而出。而这一切的背后,离不开其强大的操作系统——iOS。iOS系统不仅为iPhone提供了强大的性能支持,更通过不断创新和升级&a…...
计算机网络:传输控制协议(Transmission Control Protocol-TCP协议
计算机网络:传输控制协议(Transmission Control Protocol-TCP协议) 本文目的前置知识点TCP协议简介主要特性通信流程1. 建立连接的过程(三次握手,243)1.1 为什么要三次握手,两次不行吗? 2. 释放连接的过程(…...
东莞工业品网站建设/如何做平台推广赚钱
编写函数,参数是两个非负整数n和m,返回组合数 , 其中m<n<25。例如,n25,m12时答案为5200300。转载于:https://www.cnblogs.com/jjzzx/p/5338052.html...
长沙com建站网站设计/网络营销策划的主要特点
说到敏感词,小伙伴们可能就要吐糟了吧! 刚好最近做一个电商评论模块,简单的总结一下! 由于天朝的规则比较复杂,在评论的时候要求稍微会高一些,所以评论区都要进行后台敏感词过滤。 而对于敏感词的大致分为&…...
汕头网站设计开发专业/百度的主页
1、说说软件的测试流程? 网上都比较详细,写的比较简单,主要屡屡思路,方便记忆和复习。 需求(做什么)–计划(怎么做)–用例(具体怎么做)–执行(做…...
要服务网站建设/优化网站链接的方法
DDD进行调试 介绍标题:嵌入式开发中使用DDD进行调试2008-01-17 19:57:28在嵌入式程序开发过程中,程序员要进行大量的调试,以此验证程序的正确性,修改潜在的错误。调试器对于程序员来说是不可或缺的必备工具。在Linux环境 中&#…...
服务器网站部署端口配置/郑州seo代理外包公司
在基于Java的内容管理系统(CMS)的世界中导航并不是最简单的任务。新的解决方案不断涌现,以帮助用户管理其网站和web应用程序上的内容。各种内容管理系统的规模、价格和可扩展性各不相同。 这里有目前使用的一些最流行的Java CMS。 Magnolia Magnolia是一个完全无头的…...
360建筑网官方网站/域名查询系统
终极版C语言(十六)—3380人已学习 课程介绍 整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精…...