Vulkan_CSM级联阴影

2023-11-06 21:50
文章标签 级联 阴影 vulkan csm

本文主要是介绍Vulkan_CSM级联阴影,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

一、Cascaded Shadow Maps

阴影贴图是在游戏引擎中广泛使用的实现实时阴影的技术。基础的阴影贴图方法对于大型场景渲染显得力不从心,很容易出现阴影抖动和锯齿边缘现象。Cascaded Shadow Maps(CSM)方法根据对象到观察者的距离提供不同分辨率的深度纹理来解决上述问题。它将相机的视锥体分割成若干部分,然后为分割的每一部分生成独立的深度贴图。

CSM通常用来在大型场景模拟太阳投射的阴影。在一张阴影贴图中捕捉所有对象需要阴影贴图具有非常高的分辨率。使用多张阴影贴图就可以解决这个问题,对于近处的场景使用较高分辨率的阴影贴图,对于远处的场景使用粗糙的阴影贴图,在两张阴影贴图过渡的地方选择其中一张使用。因为远处的对象只占画面的很少一部分像素,而近处的对象占据了画面的很大一部分,进行这样的处理显然非常合理。

因此,级联阴影贴图是一种使用多个阴影贴图来进一步提高实时渲染阴影的分辨率的技术。它通过划分视锥体并创建一个轴对齐边界框来工作,该边界框用于创建光视图投影矩阵。

它的工作原理是:

  • 计算世界空间中的截锥角
  • 找到截锥体的最长半径
  • 围绕由半径形成的球体创建一个 AABB
  • 使用 AABB 创建正交投影

如图所示可说明上述过程:
在这里插入图片描述

二、C++端实现

2.1 深度贴图创建

首先,我们需要创建将保存我们的深度值的纹理。由于我们将使用多个纹理,因此我们将创建一个单独的帧缓冲区,关闭颜色缓冲区,将我们的值存储在纹理数组中:

	/*设置深度通道使用的资源,深度图像是分层的,每一层存储一个阴影映射级联*/void prepareDepthPass(){VkFormat depthFormat = vulkanDevice->getSupportedDepthFormat(true);/*深度图创建*/VkAttachmentDescription attachmentDescription{};attachmentDescription.format = depthFormat;attachmentDescription.samples = VK_SAMPLE_COUNT_1_BIT;attachmentDescription.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;attachmentDescription.storeOp = VK_ATTACHMENT_STORE_OP_STORE;attachmentDescription.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;attachmentDescription.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;attachmentDescription.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;attachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;VkAttachmentReference depthReference = {};depthReference.attachment = 0;depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;VkSubpassDescription subpass = {};subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;subpass.colorAttachmentCount = 0;subpass.pDepthStencilAttachment = &depthReference;// 使用子传递依赖项进行布局转换std::array<VkSubpassDependency, 2> dependencies;dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;dependencies[0].dstSubpass = 0;dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;dependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;dependencies[0].dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;dependencies[1].srcSubpass = 0;dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;dependencies[1].srcStageMask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;dependencies[1].srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;VkRenderPassCreateInfo renderPassCreateInfo = vks::initializers::renderPassCreateInfo();renderPassCreateInfo.attachmentCount = 1;renderPassCreateInfo.pAttachments = &attachmentDescription;renderPassCreateInfo.subpassCount = 1;renderPassCreateInfo.pSubpasses = &subpass;renderPassCreateInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());renderPassCreateInfo.pDependencies = dependencies.data();VK_CHECK_RESULT(vkCreateRenderPass(device, &renderPassCreateInfo, nullptr, &depthPass.renderPass));/*分层深度图像和视图*/VkImageCreateInfo imageInfo = vks::initializers::imageCreateInfo();imageInfo.imageType = VK_IMAGE_TYPE_2D;imageInfo.extent.width = SHADOWMAP_DIM;imageInfo.extent.height = SHADOWMAP_DIM;imageInfo.extent.depth = 1;imageInfo.mipLevels = 1;imageInfo.arrayLayers = SHADOW_MAP_CASCADE_COUNT;imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;imageInfo.format = depthFormat;imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &depth.image));VkMemoryAllocateInfo memAlloc = vks::initializers::memoryAllocateInfo();VkMemoryRequirements memReqs;vkGetImageMemoryRequirements(device, depth.image, &memReqs);memAlloc.allocationSize = memReqs.size;memAlloc.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &depth.mem));VK_CHECK_RESULT(vkBindImageMemory(device, depth.image, depth.mem, 0));//所有深度图图层视图VkImageViewCreateInfo viewInfo = vks::initializers::imageViewCreateInfo();viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;viewInfo.format = depthFormat;viewInfo.subresourceRange = {};viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;viewInfo.subresourceRange.baseMipLevel = 0;viewInfo.subresourceRange.levelCount = 1;viewInfo.subresourceRange.baseArrayLayer = 0;viewInfo.subresourceRange.layerCount = SHADOW_MAP_CASCADE_COUNT;viewInfo.image = depth.image;VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &depth.view));// One image and framebuffer per cascade// 每个级联一个贴图和帧缓冲for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {//图层的图像视图(在深度贴图里面)这个视图用于渲染特定深度的图像层VkImageViewCreateInfo viewInfo = vks::initializers::imageViewCreateInfo();viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;viewInfo.format = depthFormat;viewInfo.subresourceRange = {};viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;viewInfo.subresourceRange.baseMipLevel = 0;viewInfo.subresourceRange.levelCount = 1;viewInfo.subresourceRange.baseArrayLayer = i;viewInfo.subresourceRange.layerCount = 1;viewInfo.image = depth.image;VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &cascades[i].view));// FramebufferVkFramebufferCreateInfo framebufferInfo = vks::initializers::framebufferCreateInfo();framebufferInfo.renderPass = depthPass.renderPass;framebufferInfo.attachmentCount = 1;framebufferInfo.pAttachments = &cascades[i].view;framebufferInfo.width = SHADOWMAP_DIM;framebufferInfo.height = SHADOWMAP_DIM;framebufferInfo.layers = 1;VK_CHECK_RESULT(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &cascades[i].frameBuffer));}// Shared sampler for cascade depth reads// 级联深度采样器VkSamplerCreateInfo sampler = vks::initializers::samplerCreateInfo();sampler.magFilter = VK_FILTER_LINEAR;sampler.minFilter = VK_FILTER_LINEAR;sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;sampler.addressModeV = sampler.addressModeU;sampler.addressModeW = sampler.addressModeU;sampler.mipLodBias = 0.0f;sampler.maxAnisotropy = 1.0f;sampler.minLod = 0.0f;sampler.maxLod = 1.0f;sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &depth.sampler));}

2.2 视锥体拆分与矩阵设置

此部分为CSM算法核心:

Nvidia在此处详细解释了计算分割距离的算法:https://developer.nvidia.com/gpugems/gpugems3,它通过使用对数和均匀分割方案来获得最佳分割距离。

代码表示:

void updateCascades(){float cascadeSplits[SHADOW_MAP_CASCADE_COUNT];float nearClip = camera.getNearClip();float farClip = camera.getFarClip();float clipRange = farClip - nearClip;float minZ = nearClip;float maxZ = nearClip + clipRange;float range = maxZ - minZ;float ratio = maxZ / minZ;// 基于视图摄像机截锥体计算分割深度// https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.htmlfor (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {float p = (i + 1) / static_cast<float>(SHADOW_MAP_CASCADE_COUNT);float log = minZ * std::pow(ratio, p);float uniform = minZ + range * p;float d = cascadeSplitLambda * (log - uniform) + uniform;cascadeSplits[i] = (d - nearClip) / clipRange;}//一旦计算出分割距离,我们将创建正交矩阵://计算每个级联的正交投影矩阵float lastSplitDist = 0.0;for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {float splitDist = cascadeSplits[i];glm::vec3 frustumCorners[8] = {glm::vec3(-1.0f,  1.0f, -1.0f),glm::vec3( 1.0f,  1.0f, -1.0f),glm::vec3( 1.0f, -1.0f, -1.0f),glm::vec3(-1.0f, -1.0f, -1.0f),glm::vec3(-1.0f,  1.0f,  1.0f),glm::vec3( 1.0f,  1.0f,  1.0f),glm::vec3( 1.0f, -1.0f,  1.0f),glm::vec3(-1.0f, -1.0f,  1.0f),};// Project frustum corners into world space 将截角投影到世界空间中glm::mat4 invCam = glm::inverse(camera.matrices.perspective * camera.matrices.view);for (uint32_t i = 0; i < 8; i++) {glm::vec4 invCorner = invCam * glm::vec4(frustumCorners[i], 1.0f);frustumCorners[i] = invCorner / invCorner.w;}for (uint32_t i = 0; i < 4; i++) {glm::vec3 dist = frustumCorners[i + 4] - frustumCorners[i];frustumCorners[i + 4] = frustumCorners[i] + (dist * splitDist);frustumCorners[i] = frustumCorners[i] + (dist * lastSplitDist);}// Get frustum center 得到平截头体中心glm::vec3 frustumCenter = glm::vec3(0.0f);for (uint32_t i = 0; i < 8; i++) {frustumCenter += frustumCorners[i];}frustumCenter /= 8.0f;float radius = 0.0f;for (uint32_t i = 0; i < 8; i++) {float distance = glm::length(frustumCorners[i] - frustumCenter);radius = glm::max(radius, distance);}radius = std::ceil(radius * 16.0f) / 16.0f;glm::vec3 maxExtents = glm::vec3(radius);glm::vec3 minExtents = -maxExtents;//我们首先将我们的归一化设备坐标乘以我们的 inverseViewProjection 矩阵以获得世界空间中的截锥角。一旦我们有了角,我们就可以在近角和对应的远角之间创建一条射线,对其进行归一化,然后将其乘以新的长度,然后我们之前的长度就成为下一个分区的起点。然后我们得到这个切片的最长半径,并将其用作我们 AABB 的基础。glm::vec3 lightDir = normalize(-lightPos);glm::mat4 lightViewMatrix = glm::lookAt(frustumCenter - lightDir * -minExtents.z, frustumCenter, glm::vec3(0.0f, 1.0f, 0.0f));glm::mat4 lightOrthoMatrix = glm::ortho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, 0.0f, maxExtents.z - minExtents.z);//我们使用位于视锥体中心的任意光方向设置我们的视图矩阵。使用 Y 轴作为向上向量。为了避免光线闪烁,我们需要创建一个舍入矩阵,以便我们以纹素大小的增量移动。你可以把它想象成找出我们需要多少移动正交矩阵,以便它与阴影贴图匹配,它是这样完成的:// 将分离距离和矩阵级联存储cascades[i].splitDepth = (camera.getNearClip() + splitDist * clipRange) * -1.0f;cascades[i].viewProjMatrix = lightOrthoMatrix * lightViewMatrix;lastSplitDist = cascadeSplits[i];}}

三、着色器实现

首先我们需要获取不同的深度贴图:

3.1 depth pass

顶点着色器:

#version 450layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;#define SHADOW_MAP_CASCADE_COUNT 4layout(push_constant) uniform PushConsts {vec4 position;uint cascadeIndex;
} pushConsts;layout (binding = 0) uniform UBO {mat4[SHADOW_MAP_CASCADE_COUNT] cascadeViewProjMat;
} ubo;layout (location = 0) out vec2 outUV;out gl_PerVertex {vec4 gl_Position;   
};void main()
{outUV = inUV;vec3 pos = inPos + pushConsts.position.xyz;gl_Position =  ubo.cascadeViewProjMat[pushConsts.cascadeIndex] * vec4(pos, 1.0);
}

片元着色器:

#version 450layout (set = 1, binding = 0) uniform sampler2D colorMap;layout (location = 0) in vec2 inUV;void main() 
{	float alpha = texture(colorMap, inUV).a;if (alpha < 0.5) {discard;}
}

此阶段为简单获取depth map图形,与常规单次Shadow Map相同,唯一不同的是第一步创建贴图的是:

...VkImageCreateInfo imageInfo = vks::initializers::imageCreateInfo();imageInfo.arrayLayers = SHADOW_MAP_CASCADE_COUNT //4;
...

所以我们在生成深度贴图的时候是得循环获取:

...for (uint32_t j = 0; j < SHADOW_MAP_CASCADE_COUNT; j++) {renderPassBeginInfo.framebuffer = cascades[j].frameBuffer;vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, depthPass.pipeline);renderScene(drawCmdBuffers[i], depthPass.pipelineLayout, cascades[j].descriptorSet, j);vkCmdEndRenderPass(drawCmdBuffers[i]);}
...

3.2 csm pass

本阶段,根据片元所在位置确定使用哪一级阴影贴图。

顶点着色器:

#version 450layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inColor;
layout (location = 3) in vec3 inNormal;layout (binding = 0) uniform UBO {mat4 projection;mat4 view;mat4 model;
} ubo;layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor;
layout (location = 2) out vec3 outViewPos;
layout (location = 3) out vec3 outPos;
layout (location = 4) out vec2 outUV;layout(push_constant) uniform PushConsts {vec4 position;uint cascadeIndex;
} pushConsts;out gl_PerVertex {vec4 gl_Position;   
};void main() 
{outColor = inColor;outNormal = inNormal;outUV = inUV;vec3 pos = inPos + pushConsts.position.xyz;outPos = pos;outViewPos = (ubo.view * vec4(pos.xyz, 1.0)).xyz;gl_Position = ubo.projection * ubo.view * ubo.model * vec4(pos.xyz, 1.0);
}

片元着色器:

#version 450#define SHADOW_MAP_CASCADE_COUNT 4layout (set = 0, binding = 1) uniform sampler2DArray shadowMap;
layout (set = 1, binding = 0) uniform sampler2D colorMap;layout (location = 0) in vec3 inNormal;
layout (location = 1) in vec3 inColor;
layout (location = 2) in vec3 inViewPos;
layout (location = 3) in vec3 inPos;
layout (location = 4) in vec2 inUV;layout (constant_id = 0) const int enablePCF = 0;layout (location = 0) out vec4 outFragColor;#define ambient 0.3layout (set = 0, binding = 2) uniform UBO {vec4 cascadeSplits;mat4 cascadeViewProjMat[SHADOW_MAP_CASCADE_COUNT];mat4 inverseViewMat;vec3 lightDir;float _pad;int colorCascades;
} ubo;const mat4 biasMat = mat4( 0.5, 0.0, 0.0, 0.0,0.0, 0.5, 0.0, 0.0,0.0, 0.0, 1.0, 0.0,0.5, 0.5, 0.0, 1.0 
);//正常获取
float textureProj(vec4 shadowCoord, vec2 offset, uint cascadeIndex)
{float shadow = 1.0;float bias = 0.005;if ( shadowCoord.z > -1.0 && shadowCoord.z < 1.0 ) {float dist = texture(shadowMap, vec3(shadowCoord.st + offset, cascadeIndex)).r;if (shadowCoord.w > 0 && dist < shadowCoord.z - bias) {shadow = ambient;}}return shadow;}//pcf
float filterPCF(vec4 sc, uint cascadeIndex)
{ivec2 texDim = textureSize(shadowMap, 0).xy;float scale = 0.75;float dx = scale * 1.0 / float(texDim.x);float dy = scale * 1.0 / float(texDim.y);float shadowFactor = 0.0;int count = 0;int range = 1;for (int x = -range; x <= range; x++) {for (int y = -range; y <= range; y++) {shadowFactor += textureProj(sc, vec2(dx*x, dy*y), cascadeIndex);count++;}}return shadowFactor / count;
}void main() 
{	vec4 color = texture(colorMap, inUV);if (color.a < 0.5) discard;//获取当前片段的视图位置的级联索引uint cascadeIndex = 0;for(uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; ++i) {if(inViewPos.z < ubo.cascadeSplits[i]) {	cascadeIndex = i + 1;}}// 阴影深度比较vec4 shadowCoord = (biasMat * ubo.cascadeViewProjMat[cascadeIndex]) * vec4(inPos, 1.0);	float shadow = 0;if (enablePCF == 1) {shadow = filterPCF(shadowCoord / shadowCoord.w, cascadeIndex);} else {shadow = textureProj(shadowCoord / shadowCoord.w, vec2(0.0), cascadeIndex);}// 平行光vec3 N = normalize(inNormal);vec3 L = normalize(-ubo.lightDir);vec3 H = normalize(L + inViewPos);float diffuse = max(dot(N, L), ambient);vec3 lightColor = vec3(1.0);outFragColor.rgb = max(lightColor * (diffuse * color.rgb), vec3(0.0));outFragColor.rgb *= shadow;outFragColor.a = color.a;// 级联颜色调试if (ubo.colorCascades == 1) {switch(cascadeIndex) {case 0 : outFragColor.rgb *= vec3(1.0f, 0.25f, 0.25f);break;case 1 : outFragColor.rgb *= vec3(0.25f, 1.0f, 0.25f);break;case 2 : outFragColor.rgb *= vec3(0.25f, 0.25f, 1.0f);break;case 3 : outFragColor.rgb *= vec3(1.0f, 1.0f, 0.25f);break;}}
}

其中片元着色器的判断尤为重要:

...//获取当前片段的视图位置的级联索引uint cascadeIndex = 0;for(uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; ++i) {if(inViewPos.z < ubo.cascadeSplits[i]) {	cascadeIndex = i + 1;}}
...

在选择正确的纹理后,您可以按照通常的方式进行阴影计算。

在这里插入图片描述
在这里插入图片描述

这篇关于Vulkan_CSM级联阴影的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/359246

相关文章

UE5 半透明阴影 快速解决方案

Step 1: 打开该选项 Step 2: 将半透明材质给到模型后,设置光照的Shadow Resolution Scale,越大,阴影的效果越好

Google Earth Engine——高程数据入门和山体阴影和坡度的使用

目录 山体阴影和坡度 对图像应用计算 应用空间减速器 高程数据 通过从“重置”按钮下拉菜单中选择“清除脚本”来清除脚本。搜索“elevation”并单击 SRTM Digital Elevation Data 30m 结果以显示数据集描述。单击导入,将变量移动到脚本顶部的导入部分。将默认变量名称“image”重命名为“srtm”。使用脚本将图像对象添加到地图: Map

Vulkan描述符、描述符Pool、Layout概念

1、DescriptorSetLayout为了组织和管理着色器资源(如缓冲区、纹理、采样器等),多个相同类型的Descriptor放在一个Layout中以优化GPU对资源的访问   //DescriptorSetLayout定义了哪些描述符Descriptor类型(Buffers、Textures、Samplers)可以包含在其中 VkDescriptorSetLayoutBinding

CSS学习9[重点]--盒子模型大小、布局稳定性、CSS3盒模型以及盒子阴影

盒子模型2 一、content宽度和高度二、盒子模型的布局稳定性三、CSS3盒模型四、盒子阴影 一、content宽度和高度 使用宽度属性和高度属性可以对盒子的大小进行控制。 width和height的属性值可以为不同单位的数值或相对于父元素的百分比%,实际工作中最常用的是像素值。 大多数浏览器都采用了W3C规范,符合CSS规范的盒子模型的总宽度和总高度的计算原则是: 外

尝试进入Vulkan的ValidationLayer中的代码调用栈

问题 当前,我遇到了一个Vulkan中的错误断点: (可以看到最后的位置是“VkLayer_khronos_validation.dll”,即Validation Layer) 这个中断可以在工程YaksueGraphics的这个提交中复现: 当我双击最后的栈时,提示没有cpp文件可供查看 这个core_validation.cpp是Validation Layer中的代码,而对于V

css-方形组件圆形阴影和动画

css-方形组件圆形阴影和动画 目录 文章目录 前言效果代码 前言 CSS特效思路:1个div包裹4个,最外层作为阴影来源,内部4个作为补齐补板。主要用到以下: border-radiusbox-shadow@keyframe, animationz-indextransitionposition, top, left 效果 代码 <!DOCTYPE html>

WebView滑动过程中去掉上下左右边界阴影(倒影)

WebView滑动过程中去掉上下左右边界阴影(倒影)   我们在用WebView加载含大量文字的html文件时,需要上滑下滑才能看到更多内容。这时会出现一个比较特别显眼的现象,就是webView滑动到顶端、底部、左右的时候出现阴影(倒影,颜色会根据设置的主题颜色变化)了,这是Android高版本自带的“特性”。   去除阴影的方法很简单,只要与ScrollView的滑动去除阴影的方法一致的操作就行

iOS xib设置阴影

原理和xib中设置边框一样,我大概说一下流程。废话不多说,看图: 相当于代码:         view.layer.shadowOffset = CGSizeMake(0, 5);         view.layer.shadowColor = [UIColor blackColor].CGColor;         view.layer.shadowOpaci

记录 PyQt6 / PySide 6 自定义边框窗口的 Bug 及可能可行的解决方案:窗口抖动和添加 DWM 环绕阴影的大致原理

前言: 本篇文章将要讨论我在前不久发表的关于 PyQt6 / PySide6 自定义边框窗口代码及内容中的问题: (终)PyQt6 / PySide 6 + Pywin32 自定义标题栏窗口 + 完全还原 Windows 原生窗口边框特效_pyside6 win32 无边框窗口-CSDN博客https://blog.csdn.net/2402_84665876/article/details/

安防监控/视频汇聚EasyCVR视频监控平台级联上级,无法播放是什么原因?

EasyCVR视频监控平台,作为一款智能视频监控综合管理平台,凭借其强大的视频融合汇聚能力和灵活的视频能力,在各行各业的应用中发挥着越来越重要的作用。EasyCVR视频汇聚平台采用先进的图像处理技术和传输协议,能够确保高清、稳定的视频传输。同时,平台支持多种主流标准协议和厂家的私有协议,具有较高的兼容性和拓展性。 有用户反馈,将视频融合汇聚平台EasyCVR级联上级平台,上级平台有时能播