【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第二篇-着色器制作】
在上一篇文章中,我们已经理顺了实现流程。
接下来,我们将在UE5中,从头开始一步一步地构建一次流程。
通过这种方法,我们可以借助一个熟悉的开发环境,使那些对着色器不太熟悉的朋友们更好地理解着色器的工作原理。
这篇文章中,我们将使用这样一个纹理,点此下载
我喜欢先实现着色器,因此先用这个贴图作为占位
这是一个体积纹理的二维展开,最终我们会用RT纹理来代替这种静态的贴图,在runtime用RT进行实时的绘制
创建材质

首先创建材质,并将混合模式调整为 半透明 ,着色模式调整为 无光照
制作基础体积着色器
一、光线步进
采样贴图并通过步进对密度进行累计

首先,我们先创建Custom节点,并添加输入

Tip:
可以复制下方代码,然后在UE中,通过输入右键鼠标,进行粘贴((InputName="Tex"),(InputName="XYFrames"),(InputName="NumFrames"),(InputName="MaxSteps"),(InputName="StepSize"),(InputName="LocalCamVec"),(InputName="CurPos"))
| 输入 | 说明 |
|---|---|
| Tex | 将要进行采样的2D切片纹理对象 |
| XYFrames | float2 切片的XY数量 |
| NumFrames | 切片总数 |
| MaxSteps | 最大步进数 |
| StepSize | 步大小 |
| LocalCamVec | 本地空间相机方向 |
| CurPos | 采样位置 |
接下来我们将编写一个 光线步进(Ray Marching) 着色器,当然在这里更准确的称呼应该是Camera Ray。
其基本原理是,让屏幕上的每个像素沿着相机的朝向发射射线。这条射线从起点到终点的过程被称为"步进(Step)"。在每一步中,我们采样纹理,如果纹理中有值,则将其累加,这个累加值称为每一步的密度。达到最大步数后,通过查看这条射线累积了多少密度,就可以确定这个像素的光线穿透程度。

在代码中粘贴如下代码,对每一步的功能进行了注释
//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{// 在当前步进位置进行纹理采样,采样的是 R 通道// PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;// 将当前采样到的密度值累加到总密度中// 乘以步长是为了将采样密度与步进距离相匹配accumdens += cursample * StepSize;// 为下次循环更新射线位置,沿着相机方向步进CurPos += -LocalCamVec * StepSize;
}//返回累计结果
return accumdens;

创建对应变量输入,并将其连接到输出,就可以查看结果了
有几个新手要注意的点:
- Tex 输入的是“纹理对象”而不是“纹理采样”
- 使用TransfromVector函数将CameraVector由场景转换为本地
- 如果你是较早的UE5版本,Custom的代码要填在"这里"
- 代码中的PseudoVolumeTexture是内建的采样函数,运作原理在第一篇
- 函数
BoundingBoxBased_0-1_UVW就像他的名字一样,输出bound体积中的每个位置,你可以将RGB连接到输出查看
他会基于Bound提供位置
二、修复伪影
当从侧方看去,会发现有伪影
| 正面 | 侧面 |
|---|---|
![]() | ![]() |
这里发生的事:

我们可以力大砖飞!
我们可以力大砖飞 的通过提高MaxSteps解决,但我们有更好的办法:

为了按照上图中的方式进行采样,我们将要使用这个函数:

他属于Volumetrics插件,如果你开启了这个插件就可以直接使用

注意:
- 如果并不想开启这个插件,接下来将制作这个函数
- 如果你启用了这个插件,也需要将此函数复制一个,因为后续需要对其修改
创建材质函数VolumeBoxIntersect

为VolumeBoxIntersect函数建立输入和输出
| 输入① | 输入说明 | 输出② | 输出说明 |
|---|---|---|---|
| Plane alignment | 对齐起始位置与平面间隔以减少采样伪影 | Box Distance | - |
| Steps | 应该使用多少步进来进行平面对齐。应与步进匹配 | Intersection Position | - |
创建Custom函数
重命名为 Ray March Cube Setup

要注意:
- 输出类型为float4
- 在这里将Custom节点重命名
输入粘贴为:
((InputName="PlaneAlignment",Input=(Expression="/Script/Engine.MaterialExpressionFunctionInput'/Engine/Transient.MaterialFunction_0:MaterialExpressionFunctionInput_1'")),(InputName="MaxSteps",Input=(Expression="/Script/Engine.MaterialExpressionFunctionInput'/Engine/Transient.MaterialFunction_0:MaterialExpressionFunctionInput_0'")),(InputName="scenedepth",Input=(Expression="/Script/Engine.MaterialExpressionSceneDepth'/Engine/Transient.MaterialFunction_0:MaterialExpressionSceneDepth_1'",Mask=1,MaskR=1)))
代码为:
float localscenedepth = scenedepth;float3 camerafwd = mul(float3(0.00000000,0.00000000,1.00000000),(float3x3)ResolvedView.ViewToTranslatedWorld);float3 depthpos = ((Parameters.CameraVector * localscenedepth) / abs( dot( camerafwd, Parameters.CameraVector ) ) );depthpos = mul(depthpos, (float3x3)WSDemote(GetWorldToLocal(Parameters))).xyz;depthpos /= 256;localscenedepth = length(depthpos);//0-1
//localscenedepth /= (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2 * scale);
//localscenedepth /= abs( dot( camerafwd, Parameters.CameraVector ) );//bring vectors into local space to support object transforms
float3 localcampos = mul(float4( DFDemote(ResolvedView.WorldCameraOrigin),1.00000000), (WSDemote(GetWorldToLocal(Parameters)))).xyz;
float3 localcamvec = -normalize( mul(Parameters.CameraVector, (float3x3)WSDemote(GetWorldToLocal(Parameters))) );//make camera position 0-1
localcampos = (localcampos / (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2)) + 0.5;float3 invraydir = 1 / localcamvec;float3 firstintersections = (0 - localcampos) * invraydir;
float3 secondintersections = (1 - localcampos) * invraydir;
float3 closest = min(firstintersections, secondintersections);
float3 furthest = max(firstintersections, secondintersections);float t0 = max(closest.x, max(closest.y, closest.z));
float t1 = min(furthest.x, min(furthest.y, furthest.z));float planeoffset = 1-frac( ( t0 - length(localcampos-0.5) ) * MaxSteps );t0 += (planeoffset / MaxSteps) * PlaneAlignment;
t1 = min(t1, localscenedepth);
t0 = max(0, t0);float boxthickness = max(0, t1 - t0);float3 entrypos = localcampos + (max(0,t0) * localcamvec);return float4( entrypos, boxthickness );
计算过程上一篇有介绍,这里不赘述,效果如下


现在我们已经完成了对伪影的修复。可以看到,原本四周存在伪影的部分现在呈现出一种“扭曲”效果,这是因为当前的 MaxSteps 设定为 16。
为了获得更好的效果,可以将 MaxSteps 适当提升到 32 或 64,这样仍能保持不错的性能。
如果不进行伪影修复,而是通过增加步数的方法(力大砖飞)来提升细节质量,通常需要将
MaxSteps调至 256 甚至更高,才能达到相同的细节水平。
而循环里目前仅仅只有三个步骤,后续扩展效果几乎不可能,因此这一步伪影处理是必须的
三、相机进入内部
现在,相机不可以进入box内部。因为着色器只在模型表面进行渲染
我们可以力大砖飞!
我们可以力大砖飞 的通过开启材质双面解决,但我们依旧有更好的办法:
创建法线向内的Box模型
使用建模模式,快速的创建一个法线向内的Box
(如果你有轴在中心的法线向内的box模型则可以跳过)
进入建模模式

如果你没找到建模模式,你需要开启引擎的插件:
创建Box

- 选择建模模式
- 选择创建功能
- 选择创建Box
- 枢纽点选择居中
- 在场景中单击,放置模型
- 点击接受创建模型
调整法线

- 保持选中模型
- 选择属性修改
- 选择法线修改
- 勾选翻转法线
- 接受修改
现在将材质放入


可以看到相机进入"内部"也不会超出模型承载的渲染范围
四、介质光吸收

将输出连入不透明度,再加上一个颜色。此时能更好的看到它的密度看上去非常“扁平”。
为此要使用布格-朗伯-比尔定律,来更好的计算介质对光的吸收
它的公式在上一篇中有介绍,这里不赘述,这里我们直接使用自带的函数BeersLaw

它将返回有多少光被材质吸收

现在看上去好多了。
虽然还没有阴影和光照,但在这之前,我们先去处理另外的问题
五、体积深度

我们目前还缺乏体积的深度,体积只是被渲染到了模型的内表面 ,通过这个对比可以看到发生了什么:
| shader | mesh |
|---|---|
![]() | ![]() |
1.禁用深度测试

现在我们为其重新制作深度
为此进入材质,禁用深度测试,接下来深度由我们说的算
2.制作深度
计算 MaxSteps
BoxDistance是框距离,相当于是射线穿透框的厚度,将其乘MaxSteps对其缩放,使用Floor取整,且使用clamp确保它不会超过限制

修改 VolumeBoxIntersect

可以看到当前的深度对的不多
现在要修复这个问题:

重新进入到 VolumeBoxIntersect 函数的 Custom

这里有几行被注释掉的内容,大体上是用于将局部场景深度转换到 0-1 范围内
为Custom增加输入 scale
scale = length(TransformLocalVectorToWorld(Parameters,float3( 1.0 , 0.0 , 0.0 )).xyz);
其蓝图也就是:

Tip:
Q:为什么不直接写进cutom?
A:尽最大可能的把计算过程写为蓝图,而不要在custom内计算
看编译后的HLSL就会知道,UE的材质编译器会对蓝图的内容进行接近疯狂的优化(而custom不会,写什么就是什么)
(后续会用蓝图重制这个custom的大部分内容,但目前先这样,当下还有很多工作没完成)
然后我们注释掉红框,并解除绿框的注释:

修改后的函数如下:

修改后的 Ray March Cube Setup 代码为:
float localscenedepth = scenedepth;float3 camerafwd = mul(float3(0.00000000,0.00000000,1.00000000),(float3x3)ResolvedView.ViewToTranslatedWorld);/*
float3 depthpos = ((Parameters.CameraVector * localscenedepth) / abs( dot( camerafwd, Parameters.CameraVector ) ) );depthpos = mul(depthpos, (float3x3)WSDemote(GetWorldToLocal(Parameters))).xyz;depthpos /= 256;localscenedepth = length(depthpos);
*///0-1
localscenedepth /= (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2 * scale);
localscenedepth /= abs( dot( camerafwd, Parameters.CameraVector ) );//bring vectors into local space to support object transforms
float3 localcampos = mul(float4( DFDemote(ResolvedView.WorldCameraOrigin),1.00000000), (WSDemote(GetWorldToLocal(Parameters)))).xyz;
float3 localcamvec = -normalize( mul(Parameters.CameraVector, (float3x3)WSDemote(GetWorldToLocal(Parameters))) );//make camera position 0-1
localcampos = (localcampos / (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2)) + 0.5;float3 invraydir = 1 / localcamvec;float3 firstintersections = (0 - localcampos) * invraydir;
float3 secondintersections = (1 - localcampos) * invraydir;
float3 closest = min(firstintersections, secondintersections);
float3 furthest = max(firstintersections, secondintersections);float t0 = max(closest.x, max(closest.y, closest.z));
float t1 = min(furthest.x, min(furthest.y, furthest.z));float planeoffset = 1-frac( ( t0 - length(localcampos-0.5) ) * MaxSteps );t0 += (planeoffset / MaxSteps) * PlaneAlignment;
t1 = min(t1, localscenedepth);
t0 = max(0, t0);float boxthickness = max(0, t1 - t0);float3 entrypos = localcampos + (max(0,t0) * localcamvec);return float4( entrypos, boxthickness );

深度已经正常,一切都好起来了…?

六、精度修复
当你试图缩放cube,你会发现它破碎了


眼看着美好的事物在你的操作下支离破碎,这可太令人崩溃了,究其原因在于

TransformVector节点的精度不足,那么我们使用Custom手搓一个使用LWC精度的替代节点吧:
- 创建Custom,命名为
TransFromVector_WorldToLocal - 添加输入
CameraVector

- 代码如下
return normalize(mul(CameraVector,(float3x3)LWCToFloat(GetPrimitiveData(Parameters).WorldToLocal)));


七、阶梯纹理修复

没有时间庆祝,接下来赶到战场的是阶梯问题。深度和缩放一旦被修复,图中画圈位置的阶梯状图形就会非常显眼。
这很明显这是由step产生的,因为我们的步(step)是预计算的,因此步与步之间就形成了这种阶梯纹理
我们可以力大砖飞!
我们可以力大砖飞 的通过增加MaxSteps数量,用更小的细分来解决,但总是有更好的办法:
实际上我们只需要在进行额外一次采样就可以了

- 使用frac取小数作为一小步
FinalStepSize输入 - 在Custom中的for后再进行一次采样
修改后代码如下:
//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{// 在当前步进位置进行纹理采样,采样的是 R 通道// PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;// 将当前采样到的密度值累加到总密度中// 乘以步长是为了将采样密度与步进距离相匹配accumdens += cursample * StepSize;// 为下次循环更新射线位置,沿着相机方向步进CurPos += -LocalCamVec * StepSize;
}//修复阶梯 额外在循环后再进行一次小步(FinalStepSize)的采样,累计到密度
CurPos -= -LocalCamVec * StepSize;
CurPos += -LocalCamVec * StepSize * FinalStepSize;
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
accumdens += cursample * StepSize * FinalStepSize;//返回累计结果
return accumdens;


到此一个最基本的体积纹理渲染就做好了
在下一篇进行【制作进阶体积着色器】,继续为其实现光照等效果
相关文章:
【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第二篇-着色器制作】
在上一篇文章中,我们已经理顺了实现流程。 接下来,我们将在UE5中,从头开始一步一步地构建一次流程。 通过这种方法,我们可以借助一个熟悉的开发环境,使那些对着色器不太熟悉的朋友们更好地理解着色器的工作原理。 这篇…...
【OceanBase 诊断调优】—— GC问题根因分析
GC 流程涉及到 RS 的状态切换和 LS 的资源安全回收,流程上较长。且 GC 线程每个租户仅有一个,某个日志流 GC Hang 死时会卡住所有其余日志流的 GC,进而造成更大的影响。 本文档会帮助大家快速定位到 GC 故障的模块,直达问题核心。…...
图像面积计算一般方法及MATLAB实现
一、引言 在数字图像处理中,经常需要获取感兴趣区域的面积属性,下面给出图像处理的一般步骤。 1.读入的彩色图像 2.将彩色图像转化为灰度图像 3.灰度图像转化为二值图像 4.区域标记 5.对每个区域的面积进行计算和显示 二、程序代码 %面积计算 cle…...
指挥平台在应急场所中的主要表现有哪些
在应对自然灾害、公共安全事件等突发危机时,指挥平台作为应急管理体系的核心枢纽,其重要性不言而喻。它不仅承载着信息的快速汇聚、精准分析与高效调度功能,更在应急场所中有一定的关键表现。接下来就跟着北京嘉德立一起了解一下。 一、信息集…...
智能养殖场人机交互检测系统源码分享
智能养殖场人机交互检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Co…...
数据集-目标检测系列-海洋鱼类检测数据集 fish>> DataBall
数据集-目标检测系列-海洋鱼类检测数据集 fish>> DataBall 数据集-目标检测系列-海洋鱼类检测数据集 fish 数据量:1W 数据项目地址: gitcode: https://gitcode.com/DataBall/DataBall-detections-100s/overview github: https://github.com/…...
网络威慑战略带来的影响
文章目录 前言一、网络威慑的出现1、人工智能带来的机遇二、网络空间的威慑困境1、威慑概念的提出2、网络威慑的限度3、人类对网络威胁的认知变化4、网络空间的脆弱性总结前言 网络威慑是国家为应对网络空间风险和威胁而采取的战略。冷战时期核威慑路径难以有效复制至网络空间…...
决策树算法在机器学习中的应用
决策树算法在机器学习中的应用 决策树(Decision Tree)算法是一种基本的分类与回归方法,它通过树状结构对数据进行建模,以解决分类和回归问题。决策树算法在机器学习中具有广泛的应用,其直观性、易于理解和实现的特点使…...
Leetcode面试经典150题-39.组合总数进阶:40.组合总和II
本题是扩展题,真实考过,看这个题之前先看一下39题 Leetcode面试经典150题-39.组合总数-CSDN博客 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数…...
ProcessOn为什么导出有水印!!!(利用SVG转PNG)
processon-svg2png ProcessOn 一个非常好用的思维导图网站,但是为什么导出有水印!!!。 功能 支持按钮拖拽支持将流程图svg 转成 png下载支持修改自定义文字下载svg(开发中) 安装/使用方法 安装并使用…...
插入、更新与删除MySQL记录
在现代应用开发中,数据库操作是非常重要的一环。作为程序员,熟练掌握数据库的增删改功能,能够更有效地管理数据并提高开发效率。 本课程将围绕插入、更新与删除记录这三个操作展开,涵盖SQL中的常见语句:INSERT INTO、UPDATE 和 DELETE,并结合实际应用中的常见问题讨论如…...
【ARM】armv8的虚拟化深度解读
Type-1 hypervisor Type-1虚拟化也叫做Bare metal, standalone, Type1 Type2 hypervisor Type-2虚拟化也叫做hosted, Type-2 VM和vCPU(虚拟机和虚拟cpu) 在一个VM(虚拟机)中有多个vCPU,多个vCPU可能属于同一个Vritual Processor。 EL2…...
9/24作业
1. 分文件编译 分什么要分文件编译? 防止主文件过大,不好修改,简化编译流程 1) 分那些文件 头文件:所有需要提前导入的库文件,函数声明 功能函数:所有功能函数的定义 主函数:main函数&…...
Leetcode 106. 从中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。 示例 1: 输入:inorder [9,3,15,20,7], postorder [9,15,7,20,3] 输出:[3…...
针对考研的C语言学习(定制化快速掌握重点1)
1.printf函数的几个要点 printf函数中所有的输出都是右对齐的,除非在%后面添加负号,则表示左对齐 #include<stdio.h> int main() {int num 10;int nums 100;float f 1000.2333333333;printf("%3d\n", nums);//%3d表示输出的总宽度至…...
【大数据入门 | Hive】DDL数据定义语言(数据库DataBase)
1. 数据库(DataBase) 1.1 创建数据库 语法: CREATE DATABASE [IF NOT EXISTS] database_name [COMMENT database_comment] [LOCATION hdfs_path] [WITH DBPROPERTIES (property_nameproperty_value, ...)]; 案例: (1)创建一个…...
CNVD漏洞和证书挖掘经验总结
前言 本篇文章主要是分享一下本人挖掘CVND漏洞碰到的一些问题,根据过往成功归档的漏洞和未归档的漏洞总结出的经验,也确实给审核的大佬们添了很多麻烦(主要真的没人教一下,闷着头尝试犯了好很多错误,希望各位以后交一个…...
阿里rtc旁路推流TypeScript版NODE运行
阿里云音视频服务云端录制typescript版本; 编译后可以使用 node index.js运行 package.json 版本 // npm install --save alicloud/rtc201801112.3.0 "alicloud/rtc20180111": "^2.3.0",引入 import Client, { StartCloudRecordRequest, StopCloudRecord…...
计算机书籍分享
0.简介 数据库系统概念、深入理解计算机系统、领域驱动设计、Linux高性能服务器编程 高清版本pdf 1.链接 数据库系统概念: 链接: https://pan.baidu.com/s/17zz7QFevV2Eni9qHJyLEGA 提取码: wfrx 深入理解计算机系统 链接: https://pan.baidu.com/s/19yiJG8GqHJR…...
处理ASAM-MDF格式的开源python库asammdf
asammdf是一个强大的Python库,专为处理ASAM(Association for Standardization of Automation and Measuring Systems)MDF(Measurement Data Format)文件而设计。MDF是一种用于存储测量和诊断数据的标准格式,…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Go语言多线程问题
打印零与奇偶数(leetcode 1116) 方法1:使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...










