ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity免费Voxel引擎实战:从开源框架到可交互体素世界

Unity免费Voxel引擎实战:从开源框架到可交互体素世界 1. 项目概述为什么你需要一个Voxel引擎如果你是一个Unity开发者并且对《我的世界》、复古风格的像素游戏或者任何需要动态生成和修改地形、建筑的游戏类型感兴趣那么“Voxel引擎”这个词对你来说一定不陌生。简单来说Voxel体素就是三维空间中的一个像素点你可以把它想象成一个微小的方块。一个Voxel引擎的核心就是高效地管理、渲染和操作海量通常是数百万甚至数十亿个这样的方块。几年前想要在Unity里实现类似的效果要么自己从零开始写一套复杂的算法处理八叉树、网格合并、LOD细节层次要么就得花一笔不小的费用购买商业插件。但现在情况不同了得益于开源社区的贡献我们有了几个非常优秀的免费Voxel引擎方案。今天我要分享的就是我在多个项目中亲测、筛选后认为最适合大多数开发者上手和使用的免费Voxel引擎解决方案并附上从零开始的安装、配置到实现第一个可破坏地形的完整指南。这个指南的目标是让你在半小时内就能在Unity里跑起来一个基础的、可交互的Voxel世界。无论你是想做一个简单的沙盒原型还是为你的大型项目寻找地形系统技术方案这篇文章都能给你提供一条清晰的路径。我会重点讲解引擎的选择逻辑、安装过程中的“坑”以及如何用最少的代码实现核心功能。2. 核心引擎选型Unity Voxel生态中的免费利器面对“Unity Voxel引擎”这个需求直接去Asset Store搜索可能会让你眼花缭乱。实际上对于学习和原型开发我们完全有优秀的免费选择。经过实际测试和对比我主要推荐以下两个方向它们代表了两种不同的技术路径和复杂度。2.1 方案一MagicaVoxel Unity 插件链艺术向/快速原型这不是一个单一的引擎而是一个高效的工作流组合。MagicaVoxel是一款极其轻量、强大且完全免费的体素建模软件它的.vox文件格式已经成为体素艺术的事实标准。核心组件MagicaVoxel用于创作模型。你可以用它轻松搭建房屋、角色、树木等任何体素资产。Unity插件你需要一个能将.vox文件导入Unity并渲染的插件。这里我强烈推荐Voxel Importer或Qubicle Voxel Importer的免费版本后者通常也支持.vox格式。这些插件在Asset Store上可以找到免费的或功能受限的版本对于导入静态模型完全够用。为什么选它上手极快MagicaVoxel学习曲线平缓半小时就能做出像样的模型。导入Unity后直接就是Prefab无需处理体素数据逻辑。艺术表现力强MagicaVoxel的渲染和光照效果非常出色适合风格化、色彩丰富的项目。轻量级你只是在Unity中渲染一个合并好的网格没有运行时体素数据结构的开销。局限性非“真”引擎它不适合需要运行时动态修改地形如挖矿、建造的场景。你导入的是一个静态模型。大规模地形管理弱难以实现无限地形、LOD和动态加载。注意如果你只是想快速制作体素风格的美术资源并放入Unity场景这个方案是首选。但如果你需要“我的世界”那样的可交互世界请继续看方案二。2.2 方案二Unity Voxel Engine (UVE) 或类似开源框架程序向/动态世界这才是真正意义上的“Voxel引擎”。它们会在运行时用代码管理一个基于三维数组或稀疏数据结构如字典的体素世界并动态生成网格。核心推荐Unity Voxel Engine (UVE)。这是一个在GitHub上开源的项目搜索UnityVoxelEngine或类似名称它提供了完整的区块Chunk管理、网格生成Greedy Meshing算法用于优化面数、简单的光照和碰撞体生成。为什么选它真正的动态性你可以在运行时任意添加、删除、修改体素世界会实时更新。学习价值高通过阅读和修改其源码你能深入理解区块加载、网格优化、数据序列化等Voxel引擎的核心概念。完全免费与可定制开源协议允许你任意修改以适应项目特殊需求。备选参考Voxelmetric、Yet Another Voxel Engine (YAVE)等也是GitHub上活跃的开源项目架构各有特点可以作为技术研究的备选。局限性需要编程能力你需要理解C#和基本的3D数学知识。功能相对基础相比成熟的商业插件如Voxel Farm、MicroSplat的体素模块开源引擎在高级特性如复杂光照、水体、植被系统上可能有所欠缺需要自己扩展。性能优化是挑战处理超大规模世界时内存和CPU优化需要开发者投入精力。选型结论对于本指南我们将以方案二开源Unity Voxel Engine作为主线进行讲解因为它最能体现“引擎”二字的含义并且能满足大多数人对可交互体素世界的想象。我们会选择一个结构清晰、文档相对齐全的UVE分支作为实操案例。3. 环境准备与引擎获取在开始动手之前我们需要把“舞台”搭好。这里我假设你已安装好Unity Hub和Unity编辑器推荐使用最新的LTS版本如2022.3 LTS稳定性有保障。3.1 创建新项目与初始设置新建项目打开Unity Hub创建一个新的3D核心模板项目。给项目起一个清晰的名字例如MyVoxelWorld。关键设置项目创建后建议进行以下调整颜色空间进入Edit - Project Settings - Player在Other Settings下将Color Space从默认的Gamma改为Linear。线性空间渲染对于体素世界的光照和颜色混合更加准确视觉效果更好。输入系统可选但推荐如果你计划实现第一人称控制器与体素世界的交互如破坏方块可以考虑使用新的Input System包。在Package Manager中搜索并安装Input System。这会让后续的输入处理更现代化但本指南为简化会暂时使用旧的Input Manager。3.2 获取并导入开源Voxel引擎我们不会从零造轮子而是站在巨人的肩膀上。这里我以一个在GitHub上名为“SimpleUnityVoxelEngine”的示例项目为蓝本这是一个虚构的典型结构实际操作时请搜索类似项目。寻找引擎打开浏览器访问GitHub。在搜索栏输入关键词例如 “unity voxel engine chunk greedy meshing”。寻找一个星标较多、最近有更新、代码结构清晰的项目。下载源码找到合适的项目后点击Code - Download ZIP将整个项目源码下载到本地。导入Unity不要解压后只拷贝部分文件。更稳妥的做法是在你的Unity项目根目录下与Assets文件夹同级创建一个新文件夹如ThirdParty。将下载的ZIP包解压将其中的核心脚本文件夹通常名为Scripts、Runtime或直接是项目名拖拽到Unity项目的Assets文件夹下。确保相关的Shader、Prefab等资源也一并导入。另一种更规范的方式是使用Git Submodule如果你熟悉Git或者通过Unity的Package Manager从Git URL安装如果该项目提供了package.json。但对于初次接触直接拖拽是最快的。解决编译错误导入后Unity编辑器可能会开始编译。常见的错误包括命名空间冲突如果引擎代码的命名空间与你现有项目冲突你可能需要批量修改引擎源码的命名空间使用IDE的重构功能。缺失的API如果引擎是为旧版Unity编写可能会使用一些已过时的API。错误控制台会给出提示通常按照提示替换为新API即可例如将WWW替换为UnityWebRequest。Shader错误体素引擎通常自带Shader。如果报Shader错误检查一下Shader使用的渲染管线。如果是为内置渲染管线编写的而你使用的是URP或HDRP则需要转换Shader或切换回内置管线。对于初次尝试强烈建议在项目创建时就使用内置渲染管线以最大程度兼容。4. 核心模块解析与初步运行成功导入引擎后你的项目Assets里应该多出一堆脚本和资源。别慌我们一步步拆解。一个典型的轻量级Voxel引擎会包含以下几个核心模块4.1 世界管理器 (World/Chunk Manager)这是引擎的大脑负责管理整个体素世界。它通常将世界划分为一个个固定大小的“区块”(Chunk)例如16x256x16宽x高x深。这样做是为了性能优化我们只更新玩家周围发生变化的区块而不是整个世界的网格。核心脚本查找名为World.cs、ChunkManager.cs或VoxelWorld.cs的脚本。它做了什么维护一个字典Dictionary或三维数组用来存储所有已加载区块的引用。根据玩家的位置动态加载Instantiate或卸载Destroy区块GameObject。协调区块之间的数据更新比如挖掉一个区块边缘的方块相邻区块的网格需要重新生成。4.2 区块 (Chunk)这是世界的基本构成单元。每个区块是一个独立的GameObject挂载着Chunk.cs脚本和一个Mesh Filter、Mesh Renderer、Mesh Collider。核心脚本Chunk.cs。数据结构在Chunk.cs内部通常会有一个三维字节数组byte[,,] voxels或int[,,]用来存储这个16x256x16空间内每个位置的方块类型0代表空气1代表泥土2代表石头...。核心工作流数据填充Start()或Init()方法中会根据某种算法如柏林噪声填充voxels数组生成地形。网格生成调用一个GenerateMesh()方法。这个方法会遍历voxels数组对于每一个非空气的方块检查它的六个面是否挨着空气。只有暴露在外的面才需要生成一个四边形两个三角形添加到网格中。这就是“贪婪网格”Greedy Meshing算法的简化版思想它能将相邻且同类型的面合并成大面极大减少顶点和三角形数量。组件赋值将计算出的顶点、三角形、UV数据赋值给Mesh Filter.mesh同时用这个mesh生成Mesh Collider这样玩家就能与地形发生碰撞了。4.3 方块类型库 (Block Database)这是一个定义“世界上有哪些方块”的地方。通常是一个ScriptableObject文件或一个静态类。核心文件BlockDatabase.cs或BlockType.cs。包含信息每个方块类型ID对应一个结构体或类里面定义了string name方块名称。bool isSolid是否实体决定是否生成网格和碰撞。Vector2[] uvs在纹理图集Texture Atlas上对应的UV坐标用于贴图。float durability挖掘耐久度可选。4.4 玩家交互控制器 (Player Interaction)这是连接玩家输入与世界修改的桥梁。通常挂在玩家角色如第一人称控制器上。核心脚本VoxelInteraction.cs或PlayerDig.cs。工作原理从摄像机中央发射一条射线Raycast。如果射线击中了带有Mesh Collider的区块通过碰撞点坐标和区块的变换矩阵反算出被击中方块的局部坐标即在voxels数组中的索引。根据玩家输入如鼠标左键将对应位置的方块类型ID设置为0空气然后调用该区块的GenerateMesh()方法并通知相邻的区块如果被击中的是边缘方块也更新网格。这样一个“挖矿”效果就实现了。实操第一步让世界动起来在场景中找到一个名为World或VoxelTerrain的Prefab将其拖入场景。如果没有Prefab可能需要手动创建一个空GameObject将World.cs脚本挂上去。在Inspector面板中配置World脚本的参数如Chunk Prefab指向Chunk的PrefabView Distance渲染距离例如5表示加载玩家周围5个区块半径内的区域。将Unity自带的第一人称控制器Assets - Import Package - Characters导入或使用一个简单的自带摄像机脚本的胶囊体作为玩家。运行游戏。你应该能看到一个由方块构成的基础地形。使用WASD移动移动时观察场景Hierarchy会发现新的ChunkGameObject在动态生成和销毁。5. 深入实操从零构建一个迷你体素世界现在我们假设导入的引擎只是一个框架或者你想更深入地理解每一个环节。我们来尝试用最核心的代码自己搭建一个微型的、可交互的体素世界。5.1 创建方块类型与纹理图集准备纹理找一张方格纸一样的纹理或者用画图工具制作一个简单的纹理图集。例如创建一个64x64的图片平均分成4个32x32的格子分别涂上草绿色、土棕色、灰白色、深灰色代表草皮、泥土、石头、煤块。保存为PNG格式导入Unity将Texture Type设置为Sprite (2D and UI)并勾选Read/Write Enabled。创建BlockData ScriptableObject// BlockData.cs using UnityEngine; [CreateAssetMenu(fileName New Block, menuName Voxel/Block Data)] public class BlockData : ScriptableObject { public byte id; public string blockName; public bool isSolid true; public Vector2Int atlasOffset; // 在图集中的偏移以格子为单位 }然后在Assets中右键创建几个BlockData资产分别配置好。5.2 实现核心的Chunk脚本这是最复杂也最核心的一步。下面是一个极度简化的Chunk脚本框架用于阐述原理// SimpleChunk.cs using UnityEngine; public class SimpleChunk : MonoBehaviour { public static int CHUNK_SIZE 16; public byte[,,] voxelMap new byte[CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE]; private MeshFilter meshFilter; private MeshCollider meshCollider; private ListVector3 vertices new ListVector3(); private Listint triangles new Listint(); private ListVector2 uvs new ListVector2(); void Start() { meshFilter GetComponentMeshFilter(); meshCollider GetComponentMeshCollider(); GenerateTerrain(); // 用噪声生成地形数据 GenerateMesh(); } void GenerateTerrain() { for (int x 0; x CHUNK_SIZE; x) { for (int z 0; z CHUNK_SIZE; z) { // 简单的高度图生成中间高四周低 int height Mathf.RoundToInt(10 Mathf.PerlinNoise(x * 0.1f, z * 0.1f) * 5); for (int y 0; y CHUNK_SIZE; y) { if (y height - 3) voxelMap[x, y, z] 2; // 石头 else if (y height) voxelMap[x, y, z] 1; // 泥土 else if (y height) voxelMap[x, y, z] 0; // 草皮 (假设id0) else voxelMap[x, y, z] 0; // 空气 } } } } void GenerateMesh() { vertices.Clear(); triangles.Clear(); uvs.Clear(); for (int x 0; x CHUNK_SIZE; x) { for (int y 0; y CHUNK_SIZE; y) { for (int z 0; z CHUNK_SIZE; z) { byte voxelID voxelMap[x, y, z]; if (voxelID 0) continue; // 跳过空气方块 // 检查六个邻居如果邻居是空气则生成这个面 // 这里仅以“上”面为例 if (IsVoxelAir(x, y 1, z)) { AddVoxelFaceToMesh(new Vector3(x, y, z), VoxelFace.Top, voxelID); } // ... 同样处理其他五个面Bottom, Left, Right, Front, Back } } } Mesh mesh new Mesh(); mesh.vertices vertices.ToArray(); mesh.triangles triangles.ToArray(); mesh.uv uvs.ToArray(); mesh.RecalculateNormals(); // 重要用于光照计算 meshFilter.mesh mesh; meshCollider.sharedMesh mesh; } bool IsVoxelAir(int x, int y, int z) { // 简化处理检查是否在边界内且为空气 if (x 0 || x CHUNK_SIZE || y 0 || y CHUNK_SIZE || z 0 || z CHUNK_SIZE) return true; // 边界外视为空气这样就会生成最外层的面 return voxelMap[x, y, z] 0; } void AddVoxelFaceToMesh(Vector3 pos, VoxelFace face, byte blockID) { int vertIndex vertices.Count; // 根据面类型添加4个顶点坐标需要加上区块的世界坐标偏移 Vector3[] faceVertices GetFaceVertices(pos, face); vertices.AddRange(faceVertices); // 添加两个三角形6个索引构成一个四边形面 triangles.Add(vertIndex); triangles.Add(vertIndex 1); triangles.Add(vertIndex 2); triangles.Add(vertIndex); triangles.Add(vertIndex 2); triangles.Add(vertIndex 3); // 根据blockID和图集信息计算UV坐标并添加 Vector2[] faceUVs CalculateUVsForFace(blockID, face); uvs.AddRange(faceUVs); } // ... 其他辅助方法GetFaceVertices, CalculateUVsForFace 等 } public enum VoxelFace { Top, Bottom, Left, Right, Front, Back }实操心得上面的GenerateMesh函数是最基础的“笨”方法遍历每个方块和每个面。在实际引擎中必须使用“贪婪网格”(Greedy Meshing)算法来优化。它的核心思想是在同一层Y层中将X-Z平面上相邻且同类型的方块面合并成更大的矩形从而将成千上万个小四边形合并成几十个大四边形性能提升是数量级的。实现贪婪网格是编写高效Voxel引擎的第一个真正的挑战。5.3 实现简单的玩家交互创建一个VoxelEditor.cs脚本挂载到主摄像机上。// SimpleVoxelEditor.cs using UnityEngine; public class SimpleVoxelEditor : MonoBehaviour { public Camera playerCamera; public float reachDistance 5f; public byte removeBlockID 0; // 空气ID public byte placeBlockID 1; // 泥土ID void Update() { if (Input.GetMouseButtonDown(0)) // 左键挖除 { EditVoxel(false); } if (Input.GetMouseButtonDown(1)) // 右键放置 { EditVoxel(true); } } void EditVoxel(bool place) { Ray ray playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); RaycastHit hit; if (Physics.Raycast(ray, out hit, reachDistance)) { // 找到被击中的Chunk SimpleChunk chunk hit.collider.GetComponentSimpleChunk(); if (chunk ! null) { // 计算击中点在Chunk局部空间的位置 Vector3 localHitPoint hit.point - hit.normal * 0.5f; // 稍微向内偏移以定位到被击中的方块中心 Vector3 chunkLocalPos chunk.transform.InverseTransformPoint(localHitPoint); int x Mathf.FloorToInt(chunkLocalPos.x); int y Mathf.FloorToInt(chunkLocalPos.y); int z Mathf.FloorToInt(chunkLocalPos.z); if (place) { // 放置在击中点沿着法线方向偏移一格的位置放置方块 Vector3 placePos hit.point hit.normal * 0.5f; Vector3 chunkPlaceLocalPos chunk.transform.InverseTransformPoint(placePos); int px Mathf.FloorToInt(chunkPlaceLocalPos.x); int py Mathf.FloorToInt(chunkPlaceLocalPos.y); int pz Mathf.FloorToInt(chunkPlaceLocalPos.z); // 检查位置是否有效且为空 if (IsInChunkBounds(px, py, pz) chunk.voxelMap[px, py, pz] 0) { chunk.voxelMap[px, py, pz] placeBlockID; chunk.GenerateMesh(); // 重新生成网格 } } else { // 挖除 if (IsInChunkBounds(x, y, z) chunk.voxelMap[x, y, z] ! 0) { chunk.voxelMap[x, y, z] removeBlockID; chunk.GenerateMesh(); // 重新生成网格 } } } } } bool IsInChunkBounds(int x, int y, int z) { return x 0 x SimpleChunk.CHUNK_SIZE y 0 y SimpleChunk.CHUNK_SIZE z 0 z SimpleChunk.CHUNK_SIZE; } }将这段代码挂载到你的主摄像机或玩家对象上运行游戏。你现在应该可以用鼠标左键挖掉方块用右键放置方块了。这就是一个最最基础的、可交互的体素世界的雏形。6. 性能优化与高级特性入门当你成功运行起基础世界后下一步就是让它变得更高效、更丰富。这里有几个关键方向6.1 多线程与作业系统网格生成GenerateMesh是一个CPU密集型任务如果在主线程进行当修改大量方块时会造成游戏卡顿。解决方案使用C# Job System和Burst Compiler。你可以将遍历体素数据、计算顶点和三角形列表的逻辑封装成一个IJob作业。主线程只负责提交作业和将结果应用回Mesh。这能极大提升性能尤其是对于大型区块。注意点Job System要求数据是NativeArray这样的原生容器你需要将voxelMap等数据转换过去。同时网格数据的应用mesh.vertices ...必须在主线程完成。6.2 细节层次LOD当区块距离玩家很远时我们不需要渲染高精度的网格。可以为每个区块实现多个LOD级别例如距离每增加一定范围区块的网格生成就使用更低的采样率比如将2x2x2的方块合并成一个大方块。实现思路在WorldManager中根据区块与玩家的距离决定其LOD等级。Chunk脚本根据当前LOD等级采用不同的算法如跳过某些方块的检查来生成简化版的网格。6.3 光照与阴影简单的体素世界通常使用顶点颜色或简单的环境光遮蔽AO来模拟光照。环境光遮蔽AO在生成网格时检查每个顶点相邻的三个方块是否被填充。如果被填充的角落越多这个顶点的颜色就越暗。这能极大地增强体素世界的立体感和深度让方块之间看起来不是简单地粘在一起。动态光源实现真正的动态光源如手电筒、火把比较复杂需要在生成网格时计算每个顶点受到的光照影响或者使用延迟渲染。一个取巧的办法是使用Unity的实时点光源Point Light但性能开销大数量需严格控制。6.4 无限地形与数据持久化无限地形WorldManager需要根据玩家移动的坐标不断在边缘生成新的区块并卸载身后的区块。关键是要有一个种子Seed来确保无论玩家走到哪里用相同噪声函数生成的地形都是一致的。保存与加载将每个区块的voxelMap数据序列化成字节数组保存到文件或数据库中。当玩家再次进入该区块时优先从存储中加载数据如果没有则用噪声函数重新生成。Unity的System.IO或PlayerPrefs适用于小数据量可以用于本地存储对于大型在线游戏则需要服务器和数据库。7. 常见问题与排查技巧实录在开发和调试Voxel引擎的过程中你几乎一定会遇到下面这些问题。我把我的踩坑记录分享出来希望能帮你节省大量时间。7.1 网格生成错误破面、缝隙或闪烁症状方块之间出现黑色缝隙Z-fighting或者面在特定角度闪烁。原因与解决浮点数精度问题这是最常见的原因。在计算顶点位置时尽量避免使用世界坐标直接相加。使用Vector3Int或对float坐标进行Mathf.FloorToInt取整确保顶点坐标是精确的。在生成面时顶点坐标可以稍微向内收缩一个极小的值如0.001避免两个完全共面的面发生深度冲突。法线计算错误调用mesh.RecalculateNormals()是必须的。如果自定义了法线请确保方向正确。背面剔除确保你生成的三角形顶点顺序是顺时针的从摄像机看向面的正面时。Unity默认是背面剔除顺序错了面就看不见。7.2 性能瓶颈帧率低下移动卡顿症状生成新地形或修改大量方块时游戏明显卡顿。排查与优化使用Profiler打开Unity的Profiler窗口查看CPU占用最高的函数。大概率是GenerateMesh。实施贪婪网格这是最大的性能提升点。将朴素的每方块6面遍历改为按层、按方向合并。分帧生成不要在一帧内更新所有被修改的区块。维护一个需要更新的区块队列每帧只处理其中几个。使用对象池区块GameObject的创建和销毁很耗时。使用对象池来复用远离视线的区块只是重置其位置和数据。7.3 交互不准射线检测挖不到/放不对位置症状鼠标点击的位置和实际被修改的方块对不上或者放置方块的位置很奇怪。调试技巧可视化射线在EditVoxel函数中使用Debug.DrawRay(ray.origin, ray.direction * reachDistance, Color.red, 1f);画出射线看看它是否指向了你认为的位置。检查坐标转换hit.point是世界坐标hit.normal是法线。计算方块局部索引时取整 (Mathf.FloorToInt) 的方向和偏移量±0.5非常关键。我的经验是对于挖除用hit.point - hit.normal * 0.5f对于放置用hit.point hit.normal * 0.5f。然后转换到局部坐标再取整。考虑碰撞体精度确保Mesh Collider的网格是更新后的。有时网格更新了但碰撞体没有立即更新MeshCollider.sharedMesh是引用赋值后即更新。7.4 内存占用过高症状随着探索区域扩大游戏内存占用不断上升。管理策略稀疏数据结构不要用三维数组byte[,,]表示整个世界。使用DictionaryVector3Int, byte只存储非空气方块。或者使用更高级的数据结构如Octree。卸载不可见区块不仅卸载网格还要释放其体素数据。将数据序列化后存入磁盘或内存缓存。纹理图集所有方块共用一张大贴图而不是每个方块一种材质这是基本操作。7.5 导入的引擎报错或无法运行症状导入开源项目后Unity控制台一片红。解决步骤检查Unity版本项目要求的Unity版本可能比你用的老或新。尝试切换Unity版本。检查渲染管线99%的Shader错误源于此。确认项目是为内置管线、URP还是HDRP编写的。在Project Settings - Graphics中检查可编程渲染管线资产是否正确分配。检查缺失的包在Package Manager中查看是否有警告安装缺失的包如Input System,Mathematics,Collections等。逐行审查错误从第一个错误开始解决。常见的如API过时按照提示替换即可。命名空间冲突可以修改自己项目的命名空间。从导入一个现成的引擎到理解其每一行代码再到自己动手实现核心模块这个过程是学习Voxel技术最快的方式。免费的方案给了我们最大的学习和修改自由虽然需要投入更多时间去研究和调试但获得的深度理解是使用黑盒插件无法比拟的。当你看到自己写出的代码构建出一个可以随意编辑的方块世界时那种成就感绝对是满满的。希望这篇指南能成为你探索体素世界的第一块坚实基石。
返回列表