Unity中实现逼真水浮力效果:从物理原理到性能优化实战
1. 项目概述为什么“水浮力”是Unity开发中的硬骨头在Unity里做水很多开发者都卡在“浮力”这个环节。你可能会觉得不就是让物体在水里漂起来吗加个向上的力不就行了但实际动手你会发现事情远没这么简单。一个漂浮的箱子它应该随着波浪起伏在水面倾斜时自动调整姿态甚至在水下部分受到不同的阻力。如果只是简单给个向上的力物体会像火箭一样窜出水面或者像石头一样僵硬地杵在那里毫无真实感。这就是为什么“逼真的水浮力效果”会成为很多项目尤其是涉及航海、生存建造或物理解谜类游戏的一个技术难点。它本质上是一个物理模拟与视觉表现深度耦合的问题。你需要计算物体浸入水中的体积这本身在实时计算中就很复杂根据浸没程度施加准确的浮力同时还要处理物体与水面的碰撞、旋转扭矩以及流体阻力的模拟。网络上能找到的很多方案要么性能开销巨大要么效果过于简陋难以在移动端或WebGL平台这也是为什么“unity webgl初始化很久”会成为热词复杂的物理计算是原因之一稳定运行。这篇指南的目的就是拆解这个复杂问题提供一个从原理到实现、兼顾效果与性能的完整解决方案。无论你是想为你的开放世界游戏添加一片可交互的海洋还是为你的解谜游戏制作一个漂浮机关这里的内容都能让你避开我踩过的那些坑快速实现一个既好看又“好用”的水浮力系统。2. 核心思路从物理公式到可运行的代码实现逼真浮力的核心是阿基米德原理。但在实时渲染的游戏世界里我们无法像科学计算软件那样进行精确的流体力学模拟。因此我们的策略是用简化的物理模型去逼近真实效果并通过Shader和代码的配合来“欺骗”玩家的眼睛。2.1 浮力计算的基本模型浮力公式很简单F ρ * g * V。其中ρ是液体密度g是重力加速度V是物体排开液体的体积也就是浸入水中的体积。在Unity中重力加速度Physics.gravity是已知的通常为(0, -9.81, 0)。液体密度我们可以自定义一个参数比如1.0表示水的密度。真正的难点在于V即实时计算物体浸入水中的体积。对于一个形状复杂的网格模型进行精确的体积计算在每帧进行是不现实的。因此我们采用一个经典且高效的近似方案使用多个采样点浮力点来估算浸没体积。你可以把物体想象成一个由许多小点构成的系统。我们在物体上预设一系列的点比如在物体的底部、四周每一帧我们检查这些点相对于水面的位置。如果这个点在水面以下它就贡献一部分浮力在水面以上则不贡献。所有水下点贡献的浮力合力作用在物体的大致中心或一个计算出的平均作用点就模拟出了浮力效果。同时这些点浸没的深度不同还会产生旋转的扭矩让物体自然地摆正。2.2 方案选型为什么不用Trigger或内置物理很多新手会想用个大的水体碰撞体Trigger进入后给一个恒力不就完了或者用Unity内置的Buoyancy Effector组件简单的Trigger恒力效果非常差。物体会获得一个恒定、方向不变的力完全无视其姿态和浸没程度导致行为诡异无法模拟倾斜、翻滚等真实物理反馈。内置Buoyancy Effector这是Unity标准物理组件的一部分但它更适用于2D物理或非常简单的3D原型。它对复杂3D网格的支持有限自定义程度低且难以与自定义的水体着色器Shader进行深度交互以达到视觉与物理的统一。因此自定义脚本结合Shader计算是追求效果与灵活性的必由之路。这套方案可以让你完全控制精确控制浮力、阻力、扭矩的每一个参数。高性能通过优化采样点数量和计算频率可以在移动端和WebGL上良好运行这也是解决“unity性能优化”和“unity webgl初始化很久”相关问题的实践。强交互易于扩展比如实现不同密度液体油、岩浆、动态水位、物体部分破损进水等高级效果。3. 分步实现构建你的浮力系统我们将系统拆解为三个部分水体渲染、浮力计算脚本、以及两者的交互。3.1 第一步创建基础的水体着色器Shader水体的视觉表现是浮力效果的“舞台”。我们不需要一开始就实现无比复杂的海浪一个能提供准确世界空间高度的平静水面就是最好的起点。这里以URPUniversal Render Pipeline为例使用Shader Graph会更直观。创建Shader Graph在URP项目中创建一个新的Shader Graph命名为“Water_Basic”。输出世界位置我们需要知道水面每一个像素在世界空间中的坐标尤其是Y轴高度信息。使用Position节点将其空间设置为World。构建水面平面一个简单的水面可以就是一个在Y轴某个高度例如0的无限大平面。但在Shader中我们通过物体的世界坐标来定义。你可以创建一个Float属性_WaterLevel用来动态控制水位高度。关键输出将计算出的世界空间Y坐标Position节点的Y分量作为一个输出项。更专业的做法是我们输出一个“深度”信息即物体表面点到水面的距离。但初期我们只需让Shader能向脚本提供“此处水面高度”的信息。一个更通用的方法是将水体的世界坐标和法线等信息写入一张渲染纹理Render Texture或通过自定义缓冲区传递但这涉及更高级的渲染管线设置。为了快速实现我们采用一个更实用的方法让浮力脚本直接访问水体材质的属性。实操心得在Shader Graph中你可以添加一个Vector1属性_WaterHeight然后在脚本中通过Material.SetFloat(“_WaterHeight”, level)来动态更新全局水位。对于波浪你可以使用Simple Noise或Sine节点对世界XZ坐标进行采样叠加到_WaterHeight上这样就能得到随时间变化的波浪高度。记住脚本需要获取的是顶点变换后的世界高度而不是未经扰动的平面高度。3.2 第二步编写核心浮力计算脚本C#这是整个系统的“大脑”。我们将创建一个名为BuoyantObject的脚本挂载到需要浮起来的物体上。using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class BuoyantObject : MonoBehaviour { [Header(浮力参数)] public float waterDensity 1.0f; // 液体密度 public float waterLevel 0.0f; // 全局静态水位备选方案 public Transform[] buoyancyPoints; // 浮力采样点数组 public float pointsPower 1.0f; // 每个点的力量系数 public float dragInWater 1.0f; // 水中阻力 public float angularDragInWater 0.5f; // 水中角阻力 [Header(高级选项)] public bool useDynamicWaterHeight true; // 是否从水体获取动态高度 public Renderer waterRenderer; // 关联的水体渲染器用于动态获取高度 private Rigidbody rb; private float originalDrag; private float originalAngularDrag; void Start() { rb GetComponentRigidbody(); originalDrag rb.drag; originalAngularDrag rb.angularDrag; // 如果没指定浮力点就在物体底部自动生成一个 if (buoyancyPoints null || buoyancyPoints.Length 0) { Debug.LogWarning(未指定浮力点将在物体中心下方创建一个默认点。, this); GameObject point new GameObject(BuoyancyPoint); point.transform.SetParent(transform); point.transform.localPosition new Vector3(0, -0.5f, 0); // 假设物体中心在原点 buoyancyPoints new Transform[] { point.transform }; } } void FixedUpdate() // 物理计算放在FixedUpdate中 { bool isUnderwater false; float totalSubmergedVolume 0f; Vector3 buoyancyForce Vector3.zero; Vector3 torqueForce Vector3.zero; foreach (Transform point in buoyancyPoints) { Vector3 worldPoint point.position; // 获取该点处的水面高度 float pointWaterHeight GetWaterHeightAtPosition(worldPoint); // 计算该点浸没深度小于0表示在水下 float submersion pointWaterHeight - worldPoint.y; if (submersion 0) // 点在水面以上 { // 可以处理部分出水的情况这里简化处理为完全出水则无浮力 continue; } // 点在水下 isUnderwater true; // 浸没深度取正值 float depth Mathf.Abs(submersion); // 简化的体积贡献假设每个点代表相同体积贡献与深度成正比 float pointVolume depth * pointsPower; totalSubmergedVolume pointVolume; // 计算该点贡献的浮力方向向上 Vector3 force waterDensity * Physics.gravity.magnitude * pointVolume * Vector3.up; buoyancyForce force; // 计算扭矩浮力作用点与质心之间的偏移会产生旋转力矩 Vector3 r worldPoint - rb.worldCenterOfMass; torqueForce Vector3.Cross(r.normalized, force); } if (isUnderwater) { // 应用浮力合力 rb.AddForce(buoyancyForce, ForceMode.Force); // 应用扭矩 rb.AddTorque(torqueForce, ForceMode.Force); // 增加水中阻力 rb.drag dragInWater; rb.angularDrag angularDragInWater; } else { // 恢复空中阻力 rb.drag originalDrag; rb.angularDrag originalAngularDrag; } } float GetWaterHeightAtPosition(Vector3 worldPos) { if (useDynamicWaterHeight waterRenderer ! null) { // 方案A通过材质属性获取适用于平静水面或全局波浪 // 这里假设你的水体Shader有一个“_WaterLevel”或“_TimeBasedHeight”的属性 // 实际中你可能需要根据worldPos.xz采样一张高度图这需要更复杂的设置。 return waterRenderer.material.GetFloat(_WaterLevel); } // 方案B使用简单的全局水位适用于原型或静止水体 return waterLevel; } }脚本关键点解析buoyancyPoints这是效果的灵魂。你需要在物体上手动放置一些空物体作为采样点。对于一个大木箱在四个底角和中点各放一个点效果会很好。点的数量是性能与精度的权衡。GetWaterHeightAtPosition函数这是连接视觉水体与物理计算的关键。简单的全局水位waterLevel只能做静止水面。要实现波浪互动你需要让这个函数能返回任意worldPos位置的水面高度。这可以通过采样一个由水体Shader生成的**高度图Height Map**来实现这是更高级但效果最好的方法。作为起步你可以先使用全局水位或一个简单的正弦波函数来模拟。扭矩计算Vector3.Cross(r, force)计算了力臂r与力force的叉积结果就是使物体旋转的力矩。这是物体能在水面摆正的关键。阻力管理物体在水中运动时受到的阻力远大于空气中。动态调整Rigidbody的drag和angularDrag可以非常简单地模拟这种粘滞感。3.3 第三步配置场景与调试准备一个带刚体Rigidbody的物体比如一个Cube。为其添加BuoyantObject脚本。创建浮力点在Cube下创建几个空子物体拖拽到脚本的Buoyancy Points数组中。将这些点分布在物体底部。创建水体可以使用一个缩放很大的Quad或Plane赋予你刚才创建的“Water_Basic”材质。将这个水体的Renderer组件拖拽到脚本的Water Renderer字段如果使用动态高度。运行测试将物体放到水面以上让它下落。你应该能看到它撞击水面溅起水花如果你有粒子系统然后摇晃几下最终稳定地漂浮在水面。调整waterDensity、pointsPower和dragInWater参数来获得不同的漂浮感觉。注意事项浮力点的位置至关重要。如果所有点都集中在物体中心下部物体将很难翻转。确保点在物体浸没部分的边缘也有分布这样才能产生恢复平衡的扭矩。对于非常复杂的模型如一艘船你可能需要数十个点来获得平滑的效果。4. 效果优化与高级技巧基础系统跑通后我们可以从性能和表现力两方面进行深化。4.1 性能优化策略采样点优化LOD对于远处的漂浮物减少buoyancyPoints的数量。可以通过脚本根据物体与摄像机的距离动态启用/禁用部分采样点。计算频率优化不是每一帧都需要进行高精度的浮力计算。对于静止或缓慢移动的物体可以将计算放在一个更慢的协程Coroutine中比如每0.2秒计算一次力并持续施加。使用Jobs System与Burst如果你有大量需要浮力计算的物体比如上百个碎片buoyancyPoints的遍历计算是并行化的绝佳场景。你可以使用Unity的C# Job System和Burst Compiler热词“unity jobs burst”所指来将计算转移到多线程并获得编译优化性能提升会非常显著。这属于高级优化范畴。简化高度查询动态从Shader获取每个点的高度可能是性能瓶颈。如果水体波浪是全局的、可通过数学公式描述的如正弦波叠加直接在GetWaterHeightAtPosition中用代码计算避免访问材质属性。4.2 表现力增强技巧水面交互反馈粒子系统当浮力点快速从水下穿出水面时物体落水瞬间在该点位置播放一个水花粒子效果。贴花Decal物体与水接触的周围可以动态生成一个湿润的贴花或泡沫贴花增强视觉融合感。音效根据物体入水的速度和体积触发不同的入水音效。实现动态波浪交互真正的“圣杯”是让物体能对水面产生反作用比如船头推开波浪。这需要将浮力计算的结果如下压力反馈给水体的着色器通常通过写入一张渲染纹理Render Texture作为波浪高度图的扰动来实现。这需要你熟悉Command Buffer或URP的Renderer Features。处理部分浸没与流体阻力目前的阻力模型比较简单。更真实的模拟是根据物体浸没在水中的部分形状和速度计算一个更复杂的流体阻力。这通常需要预计算物体的横截面积或使用更复杂的流体动力学库但对于大多数游戏调整drag和angularDrag已经足够。5. 常见问题与排查实录即使按照步骤操作你也可能会遇到一些奇怪的问题。下面是我在实际项目中遇到的一些典型情况及其解决方法。问题现象可能原因排查与解决思路物体在水面疯狂旋转或抖动1. 浮力点分布过于集中或不对称。2. 扭矩力过大。3. 物理更新步长Fixed Timestep不合适。1. 检查并调整浮力点位置确保在物体底部均匀、对称分布。2. 尝试减小pointsPower或在水下部分增加更多点来分散力。3. 在Project Settings - Time中适当减小Fixed Timestep如从0.02改为0.01增加物理计算频率。物体沉底或浮起过快1.waterDensity参数设置不当。2. 物体质量Rigidbody.mass不合理。3. 浮力点贡献的体积估算不准。1. 调整waterDensity。大于1物体会更容易浮起小于1则更容易下沉。2. 检查刚体的质量是否合乎现实比例。一个木箱的质量不应和铁箱一样。3. 调试时在FixedUpdate中打印totalSubmergedVolume的值看其变化是否平滑合理。物体穿过水面或卡在水面1. 水面碰撞体缺失或设置错误。2. 浮力计算频率与物理引擎不同步。3. 水面高度获取错误。1. 确保水体GameObject有Mesh Collider如果是复杂网格或Box Collider如果是平面。2. 确保浮力计算在FixedUpdate中进行而不是Update。3. 在GetWaterHeightAtPosition函数中添加Debug绘制可视化每个采样点获取到的水面高度线检查是否与视觉水面吻合。WebGL平台性能极差或初始化慢1. 浮力点过多或计算过于复杂。2. 在Awake/Start中进行了大量计算或资源加载。1. 严格优化浮力点数量对远处物体使用LOD。2. 考虑将部分计算移至异步流程或降低频率。WebGL是单线程任何阻塞操作都会导致卡顿。确保你的水体Shader复杂度适中避免使用过多复杂的噪声和纹理采样。这与热词“unity webgl初始化很久”和“unity性能优化”直接相关。多个物体漂浮时相互穿透浮力系统只处理水与物体的关系不处理物体间的碰撞。确保所有漂浮物体的刚体都设置了合适的碰撞体Collider并且物理材质Physic Material的摩擦力Friction和弹力Bounciness设置合理。对于密集漂浮物可能需要调整刚体的碰撞检测模式Collision Detection Mode为Continuous或Continuous Dynamic以防止高速穿透。最后再分享一个小技巧调试物理效果时善用Unity的Physics Debug Visualization。在Game视图右上角的下拉菜单中开启“Colliders”和“Rigidbodies”的显示可以清晰地看到碰撞体边界和力的作用方向对于理解物体为何旋转、为何下沉非常有帮助。实现一个逼真的系统从来不是一蹴而就的它需要你反复调整参数、观察现象、理解背后的物理原理。从这个基础的浮力系统出发你已经拥有了一个强大的工具可以在此基础上创造出令人惊叹的水体交互体验。

相关新闻

2026年想给无锡学生选专业课桌椅?哪家机构靠谱看这里!

2026年想给无锡学生选专业课桌椅?哪家机构靠谱看这里!

在为无锡的学生挑选专业课桌椅时,选择一家靠谱的机构至关重要。合适的桌椅不仅能提升学生的学习体验和效率,还对学生的身体健康有着积极的影响。今天就来给大家推荐一家值得考虑的机构——无锡唯好办公,同时分享一些挑选专业课桌椅的要点。一…

2026/10/19 20:14:42 阅读更多 →
ArcGIS Pro深度学习实战:遥感影像自动化树木识别全流程解析

ArcGIS Pro深度学习实战:遥感影像自动化树木识别全流程解析

1. 项目概述:当遥感影像遇见AI,如何让计算机“看见”每一棵树在自然资源调查、城市绿化评估、森林碳汇核算等领域,精确识别和统计树木是项基础且繁重的工作。传统的人工目视解译费时费力,且主观性强。几年前,当我第一次…

2026/10/20 1:23:11 阅读更多 →
Function‑Calling、MCP、Skill 关系与异同

Function‑Calling、MCP、Skill 关系与异同

目录 1. Function‑Calling(FC,函数调用) 2. MCP Model Context Protocol(模型上下文协议) 3. Skill(Agent 技能) 4. 简明对比表 5. 完整协作链路(三者一起工作) 6…

2026/10/19 7:57:24 阅读更多 →

最新新闻

AI视觉联合ICT方案打造智能验布系统,效率提升3-5倍

AI视觉联合ICT方案打造智能验布系统,效率提升3-5倍

1. 纺织验布行业的痛点与需求拆解1.1 人工验布的瓶颈:不止是慢,更是稳不住纺织行业的验布环节,历来是产线上最依赖“人”的工序。我早期走访过不少纺织厂,验布车间里一排排老师傅坐在灯箱前,目光跟着布面走&#xff0c…

2026/10/20 1:22:58 阅读更多 →
Golay码译码C++实现:查表纠错完整链路解析

Golay码译码C++实现:查表纠错完整链路解析

简介:为满足通信、存储方向学习者掌握Golay码纠错原理与C实现的需求,这份压缩包提供了一套Visual Studio下的Golay码译码器完整工程,内容涵盖工程配置、核心算法代码及可直接运行验证的可执行程序。包内共28个文件、2.23MB,既有go…

2026/10/20 1:22:58 阅读更多 →
老 Mac 复活指南:如何用 OpenCore Legacy Patcher 免费在旧 Intel Mac 上安装最新 macOS

老 Mac 复活指南:如何用 OpenCore Legacy Patcher 免费在旧 Intel Mac 上安装最新 macOS

老 Mac 复活指南:如何用 OpenCore Legacy Patcher 免费在旧 Intel Mac 上安装最新 macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 苹果在系…

2026/10/20 1:22:58 阅读更多 →
0816飞达接入OpenPNP:硬件配置、参数校准与故障排查全解析

0816飞达接入OpenPNP:硬件配置、参数校准与故障排查全解析

简介:这套围绕0816飞达的OpenPnP贴片机开源资料包,面向中小型电子制造企业、SMT工程师和DIY爱好者,用于解决低成本贴片自动化中的飞达结构设计、固件调试与PCB制造等问题。压缩包共73个文件,大小约11.47MB,内容覆盖机械…

2026/10/20 1:22:58 阅读更多 →
STM32F0工程包解析:IAR环境下外设移植与避坑指南

STM32F0工程包解析:IAR环境下外设移植与避坑指南

简介:STM32F0x.zip 是一套基于 IAR 环境的 STM32F0 系列开发资源包,面向嵌入式入门者与进阶开发者,可用于学习 ARM Cortex-M0 内核下的外设驱动与工程组织方式。压缩包共包含 128 个文件,以 .c 源文件、.h 头文件、.xcl 链接配置为…

2026/10/20 1:22:58 阅读更多 →
一站式云原生DevOps平台:打通研发交付全链路

一站式云原生DevOps平台:打通研发交付全链路

简介:软件研发管理平台一站式云原生DevOps解决方案是一份面向企业研发团队、产品经理及DevOps转型人员的系统化演示文稿,重点阐释如何借助敏捷迭代、小步快跑的方式完成产品规划、项目管理、质量跟踪与持续交付,适合用于方案汇报、团队内训和…

2026/10/20 1:21:57 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于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 阅读更多 →