VulkanSceneGraph学习教程(四)
第 4 章 着色器准备说明本章定位Vulkan 只接受 SPIR-V而 GLSL 是开发者最常写的语言。本章说清两者关系以及 VSG 提供的两种着色器加载路径。理解它第 5、9 章的管线代码才不突兀。4.1 本章目标理解为什么 Vulkan 需要 SPIR-V掌握 VSG 的两种着色器加载方式运行时编译 / 离线预编译会用glslangValidator离线把 GLSL 编译成.spv了解vsg::ShaderCompileSettings能调哪些编译选项。4.2 前置准备已按第 3 章装好 Vulkan SDK含glslangValidator了解 GLSL 基础语法顶点/片元着色器、layout(location)限定符。4.3 为什么是 SPIR-VVulkan 的着色器阶段只接受 SPIR-V 中间码不直接处理 GLSL/HLSL 源码。原因SPIR-V 是二进制、无歧义、易校验驱动无需再解析高级语言跨语言GLSL/HLSL/MSL 都能编译成 SPIR-V跨厂商一致。因此「写 GLSL → 编译成 SPIR-V → 交给 Vulkan」是必不可少的一步。VSG 让这一步对你几乎无感知。4.4 VSG 的两种加载方式方式 A运行时编译推荐入门直接把 GLSL 字符串交给vsg::ShaderModule或vsg::ShaderStageVSG 在compile()时用自带的glslang把 GLSL 编译成 SPIR-V// 直接传 GLSL 源码VSG 内部用 glslang 编译为 SPIR-V auto vert vsg::ShaderStage::create(VK_SHADER_STAGE_VERTEX_BIT, main, R( #version 450 layout(location 0) in vec3 inPosition; void main() { gl_Position vec4(inPosition, 1.0); } ));第 5 章的三角形就是这种方式——无需任何.spv文件。对应的底层类型是vsg::ShaderModule构造ShaderModule(const std::string source, ref_ptrShaderCompileSettings hints {})会把source暂存编译时生成codeSPIR-Vstd::vectoruint32_t。方式 B离线预编译.spv把 GLSL 先用工具编译成.spv再用vsg::ShaderModule::read()从文件读取已编译的 SPIR-Vauto vert vsg::ShaderModule::read(shaders/triangle.vert.spv); // 直接读 SPIR-V适合着色器很大/很固定、或想绕开运行时编译开销、或需要在 CI 里静态校验着色器。4.5 离线编译命令用 Vulkan SDK 自带的glslangValidator# 顶点着色器 glslangValidator -V shaders/triangle.vert -o shaders/triangle.vert.spv # 片元着色器 glslangValidator -V shaders/triangle.frag -o shaders/triangle.frag.spv或用glslcGoogle 的 Shaderc 前端SDK 也常带glslc shaders/triangle.vert -o shaders/triangle.vert.spv⚠️ 记得 GLSL 顶部写#version 450且与着色器阶段匹配-S vert/glslc会自动识别扩展名。4.6 编译选项vsg::ShaderCompileSettings若用方式 A可通过ShaderCompileSettings调整运行时编译行为字段含义languageGLSL默认或HLSLtargetSPIR-V 目标版本SPIRV_1_0…SPIRV_1_5defaultVersion默认 GLSL 版本默认 450defines预定义宏集合std::setstd::stringgenerateDebugInfo生成调试信息便于 RenderDoc 查看变量名optimize是否优化 SPIR-V示例给所有着色器加一个USE_LIGHTING宏auto hints vsg::ShaderCompileSettings::create(); hints-defines.insert(USE_LIGHTING); auto frag vsg::ShaderStage::create(VK_SHADER_STAGE_FRAGMENT_BIT, main, fragSrc, hints);实战示例自定义编译设置下面是一个完整的实战代码示例展示如何创建一个包含自定义宏定义、开启调试信息、并指定目标版本的vsg::ShaderCompileSettings对象并将其应用于具体的顶点和片元着色器加载场景中#include vsg/all.h // 创建自定义编译设置 auto compileSettings vsg::ShaderCompileSettings::create(); // 1. 设置目标 SPIR-V 版本为 1.5支持 Vulkan 1.2 compileSettings-target vsg::ShaderCompileSettings::SPIRV_1_5; // 2. 设置默认 GLSL 版本为 460支持更多现代特性 compileSettings-defaultVersion 460; // 3. 添加自定义宏定义 compileSettings-defines.insert(USE_NORMAL_MAP); // 法线贴图宏 compileSettings-defines.insert(MAX_LIGHTS4); // 最大灯光数 compileSettings-defines.insert(ENABLE_DEBUG); // 调试宏 // 4. 开启调试信息便于 RenderDoc 查看变量名 compileSettings-generateDebugInfo true; // 5. 开启优化生产环境推荐 compileSettings-optimize true; // 顶点着色器源码带条件编译 std::string vertSource R( #version 460 layout(location 0) in vec3 inPosition; layout(location 1) in vec3 inNormal; layout(location 2) in vec2 inTexCoord; #ifdef USE_NORMAL_MAP layout(location 3) in vec3 inTangent; #endif layout(location 0) out vec3 fragNormal; layout(location 1) out vec2 fragTexCoord; #ifdef USE_NORMAL_MAP layout(location 2) out vec3 fragTangent; #endif void main() { gl_Position vec4(inPosition, 1.0); fragNormal inNormal; fragTexCoord inTexCoord; #ifdef USE_NORMAL_MAP fragTangent inTangent; #endif #ifdef ENABLE_DEBUG // 调试信息输出顶点位置 gl_Position.y 0.1 * sin(gl_Position.x * 10.0); #endif } ); // 片元着色器源码带条件编译 std::string fragSource R( #version 460 layout(location 0) in vec3 fragNormal; layout(location 1) in vec2 fragTexCoord; #ifdef USE_NORMAL_MAP layout(location 2) in vec3 fragTangent; layout(binding 1) uniform sampler2D normalMap; #endif layout(binding 0) uniform sampler2D diffuseMap; layout(location 0) out vec4 outColor; void main() { vec3 normal normalize(fragNormal); #ifdef USE_NORMAL_MAP // 使用法线贴图 vec3 tangent normalize(fragTangent); vec3 bitangent cross(normal, tangent); mat3 TBN mat3(tangent, bitangent, normal); vec3 normalMapValue texture(normalMap, fragTexCoord).rgb * 2.0 - 1.0; normal normalize(TBN * normalMapValue); #endif vec4 diffuse texture(diffuseMap, fragTexCoord); // 简单光照计算 vec3 lightDir normalize(vec3(0.5, 1.0, 0.5)); float diff max(dot(normal, lightDir), 0.0); #ifdef MAX_LIGHTS // 多光源支持示例 for (int i 0; i lt; MAX_LIGHTS; i) { // 实际实现中这里会有真正的灯光计算 } #endif outColor diffuse * (0.2 0.8 * diff); #ifdef ENABLE_DEBUG // 调试可视化法线方向 outColor.rgb normal * 0.1; #endif } ); // 创建着色器阶段并应用编译设置 auto vertStage vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, // 着色器阶段 main, // 入口函数名 vertSource, // GLSL 源码 compileSettings // 自定义编译设置 ); auto fragStage vsg::ShaderStage::create( VK_SHADER_STAGE_FRAGMENT_BIT, main, fragSource, compileSettings // 复用相同的编译设置 ); // 验证编译设置已生效 if (vertStage fragStage) { std::cout 顶点着色器编译设置 std::endl; std::cout - 目标版本: SPIR-V (compileSettings-target vsg::ShaderCompileSettings::SPIRV_1_5 ? 1.5 : 其他) std::endl; std::cout - GLSL 版本: compileSettings-defaultVersion std::endl; std::cout - 调试信息: (compileSettings-generateDebugInfo ? 开启 : 关闭) std::endl; std::cout - 优化: (compileSettings-optimize ? 开启 : 关闭) std::endl; std::cout - 宏定义: ; for (const auto define : compileSettings-defines) { std::cout define ; } std::cout std::endl; }关键点说明宏定义控制通过defines.insert(USE_NORMAL_MAP)添加宏着色器源码中可使用#ifdef USE_NORMAL_MAP进行条件编译。调试信息generateDebugInfo true让 RenderDoc 等工具能显示原始变量名而非优化后的临时变量。版本控制target SPIRV_1_5和defaultVersion 460确保使用较新的着色器特性。统一设置同一个ShaderCompileSettings对象可复用于多个着色器阶段保证宏定义和编译选项的一致性。条件编译示例展示了如何根据宏定义在着色器中启用/禁用特定功能如法线贴图、调试效果。这个示例可以直接集成到第 5 章的三角形程序或第 9 章的完整管线中为实际项目中的着色器条件编译和调试提供参考模板。4.7#include展开GLSL 里若用了#include common.glslVSG 提供vsg::insertIncludes(source, options)在编译前把被包含文件内联展开按Options指定的搜索路径查找。这对复用光照/材质代码片段很有用。4.8 常见问题现象原因解决运行时报SPIRV-Tools/glslang 错误GLSL 语法不支持如用了#version 300 es改为#version 450避免在 Vulkan 用桌面 GL 专用语法.spv读出来是乱码/链接失败用错编译阶段或没加-V用glslangValidator -V明确产出 SPIR-VRenderDoc 里看不到变量名没生成调试信息设hints-generateDebugInfo true多个着色器宏不一致defines没统一设把共享ShaderCompileSettings复用到多个ShaderStage4.8.1 实战错误排查示例当使用 VSG 的运行时编译方式时如果 GLSL 代码有语法错误VSG 会抛出异常。下面是一个完整的 C 示例展示如何捕获并输出详细的着色器编译错误信息#include vsg/all.h #include iostream #include stdexcept // 尝试编译有错误的 GLSL 着色器并捕获异常 void testShaderCompileError() { try { // 故意写一个有语法错误的 GLSL 顶点着色器 std::string errorVertSource R( #version 450 layout(location 0) in vec3 inPosition; // 错误1缺少分号 void main() { gl_Position vec4(inPosition, 1.0) // 这里故意漏了分号 } // 错误2使用了未定义的变量 vec3 undefinedVariable someUndefinedFunction(); // someUndefinedFunction 不存在 ); // 尝试创建着色器阶段这会触发编译 auto vertStage vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, main, errorVertSource ); // 如果编译成功不应该发生输出提示 if (vertStage) { std::cout lt;lt; 警告着色器编译竟然成功了但代码中故意包含了错误 lt;lt; std::endl; } } catch (const vsg::Exceptionamp; e) { // 捕获 VSG 异常输出详细错误信息 std::cerr lt;lt; lt;lt; std::endl; std::cerr lt;lt; VSG 着色器编译异常捕获 lt;lt; std::endl; std::cerr lt;lt; 异常类型: lt;lt; typeid(e).name() lt;lt; std::endl; std::cerr lt;lt; 异常消息: lt;lt; e.message lt;lt; std::endl; std::cerr lt;lt; lt;lt; std::endl; // 尝试从异常消息中提取 glslang 的错误信息 std::string errorMsg e.message; if (errorMsg.find(ERROR:) ! std::string::npos) { std::cerr lt;lt; \n详细错误分析 lt;lt; std::endl; // 查找错误行号信息 size_t linePos errorMsg.find(:); if (linePos ! std::string::npos amp;amp; linePos 1 lt; errorMsg.length()) { // 尝试提取行号glslang 错误格式通常是 ERROR: 0:行号: 错误描述 std::string afterColon errorMsg.substr(linePos 1); size_t secondColon afterColon.find(:); if (secondColon ! std::string::npos) { std::string lineNumStr afterColon.substr(0, secondColon); try { int lineNum std::stoi(lineNumStr); std::cerr lt;lt; 错误发生在第 lt;lt; lineNum lt;lt; 行附近 lt;lt; std::endl; // 根据行号给出具体建议 if (lineNum 7) // 对应 gl_Position 行 { std::cerr lt;lt; 建议检查第 7 行是否缺少分号 (;) lt;lt; std::endl; } else if (lineNum 11) // 对应 undefinedVariable 行 { std::cerr lt;lt; 建议检查第 11 行的函数或变量是否正确定义 lt;lt; std::endl; } } catch (...) { std::cerr lt;lt; 无法解析错误行号 lt;lt; std::endl; } } } // 输出完整的错误消息 std::cerr lt;lt; \n完整错误消息 lt;lt; std::endl; std::cerr lt;lt; errorMsg lt;lt; std::endl; } } catch (const std::exceptionamp; e) { // 捕获其他标准异常 std::cerr lt;lt; 标准异常: lt;lt; e.what() lt;lt; std::endl; } catch (...) { // 捕获所有其他异常 std::cerr lt;lt; 未知异常类型 lt;lt; std::endl; } } // 使用 VSG 的日志系统记录着色器编译信息 void logShaderCompilationInfo() { // 创建一个正确的着色器来演示成功编译的日志 std::string correctVertSource R( #version 450 layout(location 0) in vec3 inPosition; layout(location 1) in vec3 inColor; layout(location 0) out vec3 fragColor; void main() { gl_Position vec4(inPosition, 1.0); fragColor inColor; } ); // 创建编译设置并开启调试信息 auto compileSettings vsg::ShaderCompileSettings::create(); compileSettings-gt;generateDebugInfo true; std::cout lt;lt; \n lt;lt; std::endl; std::cout lt;lt; 着色器编译信息日志示例 lt;lt; std::endl; std::cout lt;lt; lt;lt; std::endl; try { auto vertStage vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, main, correctVertSource, compileSettings ); if (vertStage) { std::cout lt;lt; ✓ 着色器编译成功 lt;lt; std::endl; std::cout lt;lt; 阶段: 顶点着色器 lt;lt; std::endl; std::cout lt;lt; 入口点: main lt;lt; std::endl; std::cout lt;lt; SPIR-V 代码大小: lt;lt; vertStage-gt;module-gt;code.size() lt;lt; 字 lt;lt; std::endl; std::cout lt;lt; 调试信息: lt;lt; (compileSettings-gt;generateDebugInfo ? 已生成 : 未生成) lt;lt; std::endl; } } catch (const vsg::Exceptionamp; e) { std::cout lt;lt; ✗ 编译失败: lt;lt; e.message lt;lt; std::endl; } } int main() { std::cout 开始着色器错误排查演示... std::endl; // 演示错误捕获 testShaderCompileError(); // 演示成功编译的日志 logShaderCompilationInfo(); std::cout lt;lt; \n错误排查建议 lt;lt; std::endl; std::cout lt;lt; 1. 使用 try-catch 块包裹着色器创建代码 lt;lt; std::endl; std::cout lt;lt; 2. 捕获 vsg::Exception 类型异常 lt;lt; std::endl; std::cout lt;lt; 3. 检查异常消息中的 ERROR: 前缀和行号信息 lt;lt; std::endl; std::cout lt;lt; 4. 使用 glslangValidator 命令行工具预编译验证 lt;lt; std::endl; std::cout lt;lt; 5. 开启 generateDebugInfo 便于 RenderDoc 调试 lt;lt; std::endl; return 0; }关键错误信息解析glslang 错误格式VSG 内部使用 glslang 编译 GLSL错误消息通常包含ERROR: 0:行号: 错误描述格式。常见错误类型语法错误缺少分号、括号不匹配、关键字拼写错误语义错误未定义的变量、类型不匹配、函数参数错误版本不兼容使用了 Vulkan 不支持的 GLSL 特性资源限制超出最大 uniform 数量、纹理单元限制等处理建议将错误消息中的行号与原始 GLSL 代码行号对应注意 #version 和空行也算行使用glslangValidator -V yourShader.vert在命令行预编译验证简化着色器代码逐步添加功能定位问题检查 GLSL 版本Vulkan 需要 #version 450 或更高确保所有函数和变量都有正确定义实际应用在开发过程中可以将上述错误捕获逻辑封装到工具函数中在着色器加载失败时自动输出详细诊断信息并尝试提供修复建议。4.9 小结Vulkan 只吃 SPIR-VVSG 让 GLSL→SPIR-V 对你几乎透明方式 A 运行时编译想要离线/可控就用glslangValidator -V预编译成.spv再用ShaderModule::read()读取方式 BShaderCompileSettings控制版本、宏、调试信息insertIncludes支持 GLSL#include。4.10 延伸阅读与下一章预告第 5 章《第一个 VSG 程序》方式 A 落地三角形 运行时编译管线第 9 章《着色器与管线》ShaderModule/PipelineLayout/GraphicsPipeline的完整创建与配置。

相关新闻

如何在现代硬件上完美运行Windows 9x系统?patcher9x修复工具全攻略

如何在现代硬件上完美运行Windows 9x系统?patcher9x修复工具全攻略

如何在现代硬件上完美运行Windows 9x系统?patcher9x修复工具全攻略 【免费下载链接】patcher9x Patch for Windows 9x to fix CPU issues 项目地址: https://gitcode.com/gh_mirrors/pa/patcher9x 你是否曾在现代计算机上尝试运行经典的Windows 95、98或ME系…

2026/10/16 4:48:14 阅读更多 →
5分钟从零到一:abogen有声书制作全流程指南

5分钟从零到一:abogen有声书制作全流程指南

5分钟从零到一:abogen有声书制作全流程指南 【免费下载链接】abogen Generate audiobooks from EPUBs, PDFs and text with synchronized captions. 项目地址: https://gitcode.com/GitHub_Trending/ab/abogen abogen是一款革命性的有声书生成工具&#xff0…

2026/10/16 4:48:26 阅读更多 →
C语言中的动态内存分配

C语言中的动态内存分配

在很多场景,数据的大小是在运行时才能确定,编译阶段无法确定数据长度;或者有大块的数据需要存放,但是栈空间不够;或者某些参数需要实现跨函数传递;或者需要实现可变的数据结构。当面临这些问题时&#xff0…

2026/10/16 4:59:40 阅读更多 →

最新新闻

数据库入门核心:从表结构、索引到事务的实战解析

数据库入门核心:从表结构、索引到事务的实战解析

我在带新人入门的时候,最常听到的一句话是:“数据库到底是个啥?感觉就是个存数据的软件呗。”这话没毛病,但只对了一半。如果只是存数据,Excel也能干,为啥要搞出个专门的“王国”出来?因为数据库…

2026/10/19 21:10:50 阅读更多 →
从MLA到DeepSeekMoE:DeepSeek系列大模型架构演进与技术拆解

从MLA到DeepSeekMoE:DeepSeek系列大模型架构演进与技术拆解

很多人聊大模型架构的时候,总喜欢从Transformer开始铺开,但真正把架构演进讲透的,恰恰要看国内开源模型这几年的实操路线。我自己的感受是,2024年初那会儿大家还在卷榜单,到了年底风向就变了,变成卷单次推理…

2026/10/19 21:09:49 阅读更多 →
顶级投资人称过去只是热身:AI智能跃迁1000倍意味着什么

顶级投资人称过去只是热身:AI智能跃迁1000倍意味着什么

1. 当一位顶级投资人说出“过去只是热身”第一次看到这个访谈标题时,我的直觉反应不是“又在吹AI”,而是注意到一个细节:说这话的人,过去四十年几乎每一次都押中了时代级的技术浪潮。从个人电脑时代的操作系统与芯片,到…

2026/10/19 21:09:49 阅读更多 →
决策树在电信客户流失预测中的实战应用与规则落地

决策树在电信客户流失预测中的实战应用与规则落地

1. 先摸清业务:流失预测到底在解决什么问题接手电信客户流失预测这类项目,很多人第一反应就是找数据集、跑模型、调参出准确率。我之前做过一个运营商侧的流失预警项目,踩过不少坑之后发现,第一步根本不是建模,而是把“…

2026/10/19 21:08:47 阅读更多 →
表格合并全攻略:从Excel自带功能到代码方案一网打尽

表格合并全攻略:从Excel自带功能到代码方案一网打尽

表格合并这事儿,看起来简单,真做起来恶心过多少人,我太有数了。尤其是月底、年底那几天,群里一片哀嚎:销售数据分了十二个月、各门店表头还不一样、刘姐交上来的表编码是文本格式,张哥那边编码却是数字&…

2026/10/19 21:07:45 阅读更多 →
基于Python的UDP可靠传输实现:从停等协议到GBN滑动窗口

基于Python的UDP可靠传输实现:从停等协议到GBN滑动窗口

简介:这是一份基于Python实现的可靠数据传输协议课程设计资源包,以UDP为底层通道,完整演示了停等协议、GBN滑动窗口协议及SR协议的设计与实现过程。面向计算机网络课程学生、需要完成类似实验或对传输层协议感兴趣的学习者,可借此…

2026/10/19 21:05:42 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →