☰
从像素图到3D场景:Blender+Codex+Unity/Godot全流程还原未白镇
2026/10/2 4:52:13 网站建设 项目流程

1. 从一张像素图到可跑可跳的3D场景,这条路我走通了

「未白镇」这三个字,玩过经典像素RPG的人应该都不陌生——那个开局的小镇,几间矮房、一条土路、几棵树、一块告示牌,画面简单到只有几十个像素点,却承载了无数人的童年记忆。前阵子我突发奇想:能不能把这张2D像素图,用Codex辅助生成代码、Blender还原成3D场景,最后导入Unity或者Godot里真正跑起来?说干就干,前后折腾了大概一周,中间踩了不少坑,也总结出一套相对稳定的流程。这篇文章就把整个思路、操作步骤、参数细节和避坑经验完整分享出来,适合有一定Blender基础、想尝试2D转3D场景还原的开发者,也适合刚接触Unity或Godot、想找一个完整小项目练手的朋友。

先明确一下这个项目的核心目标:输入是一张「未白镇」风格的像素俯视图,输出是一个可以在Unity或Godot中自由漫游的3D场景,包含地形、建筑、植被、道路等基本元素,风格上保留像素游戏的复古感,但空间上是真正的三维结构。整个流程分为四个阶段:像素图分析与元素拆解、Blender建模与材质处理、Codex辅助生成导入脚本与场景配置代码、Unity/Godot中的场景组装与运行调试。每个阶段都有它的门道,下面逐个拆开讲。

2. 整体设计思路与方案选型

2.1 为什么选Blender而不是直接在引擎里搭

很多人第一反应可能是:Unity和Godot都有自己的建模工具或者ProBuilder,为什么还要绕一圈用Blender?我一开始也想过直接在引擎里搭,但实际试下来发现几个问题。第一,像素图的元素比例和位置需要精确还原,引擎自带的建模工具在精确对齐和尺寸控制上不如Blender直观;第二,Blender的修改器系统可以快速实现阵列、镜像、倒角等操作,对于重复元素(比如一排栅栏、一片树林)效率高很多;第三,Blender导出FBX或glTF的兼容性非常好,Unity和Godot都能直接吃进去,不用做额外转换。

还有一个很实际的原因:像素图的颜色是有限的、扁平的,如果直接在引擎里用默认材质,很容易出现光照过曝或者颜色偏差。Blender里可以方便地设置无光照材质或者烘焙贴图,把像素图的色板直接映射到3D模型上,保持那种复古的扁平感。这一点在后面材质部分会详细说。

2.2 Codex在整个流程里扮演什么角色

Codex不是用来建模的,它的价值在于处理那些重复性高、逻辑性强但手写又很烦的代码工作。比如:批量导入FBX模型并自动设置导入参数、根据像素图坐标生成场景物体的摆放位置、写一个简单的角色控制器让相机能在场景里漫游、处理Unity和Godot之间的API差异。这些代码本身不复杂,但涉及大量引擎特定的API调用,手写容易出错,用Codex生成初版再微调,效率能提升不少。

我实际用下来的感受是:Codex适合生成“结构明确、参数可枚举”的代码,比如“读取一个JSON文件,里面是物体名称和坐标,然后在场景里实例化对应的Prefab”。但如果你让它直接生成一个完整的游戏逻辑,它往往会过度设计或者遗漏边界情况。所以我的用法是:把需求拆成小函数,每个函数只做一件事,让Codex逐个生成,然后自己组装和测试。

2.3 Unity和Godot的选择考量

这两个引擎我都试了,各有优劣。Unity的生态更成熟,Asset Store里有大量现成的像素风Shader和工具,导入FBX的流程也很顺;Godot更轻量,启动快,GDScript写起来舒服,而且对glTF的支持是原生的,不需要额外插件。如果你只是想做一个小场景漫游Demo,Godot可能更省心;如果你后续想扩展成完整的游戏,Unity的资源和社区支持会更有优势。

我最终的方案是:Blender里统一导出glTF格式,因为Unity和Godot都能直接读取,而且glTF保留了材质和层级信息,比FBX更干净。然后在两个引擎里分别写了一套简单的导入和摆放脚本,下面会分别给出关键代码。

3. 像素图分析与元素拆解

3.1 从像素图里提取有效信息

拿到一张「未白镇」的像素图,第一步不是急着打开Blender,而是先做分析。我用的是Aseprite,当然Photoshop或者GIMP也行。把图放大到800%以上,然后做三件事:数格子、标坐标、提色板。

数格子是为了确定场景的网格尺寸。假设原图是64x64像素,每个像素代表现实中的0.5米,那整个场景就是32x32米。这个比例很重要,因为后面在Blender里建模时,所有物体的尺寸都要按这个比例来,否则导入引擎后会出现比例失调。我一般会先定一个基准:1像素=0.5米,然后根据这个算出道路宽度、房屋高度、树木间距等。

标坐标是为了确定每个元素在场景中的位置。我会在像素图上画一个网格,然后记录每个建筑、每棵树的中心点坐标。比如左下角那间房子在像素坐标(12, 8)的位置,换算成3D坐标就是(6, 0, 4)米。这个坐标表后面会直接喂给Codex生成的摆放脚本。

提色板是为了保持颜色一致。像素图的颜色数量通常很少,可能就16色或32色。我会把这些颜色提取出来,存成一个调色板文件,然后在Blender里创建对应的材质。这样建出来的模型颜色和原图完全一致,不会出现“看起来差不多但就是不对劲”的情况。

3.2 元素分类与建模优先级

像素图里的元素可以分成几类:地形(地面、道路、水域)、建筑(房屋、围墙)、植被(树木、草丛)、道具(告示牌、木箱、路灯)。不是所有元素都需要精细建模,要根据它们在场景中的视觉权重来分配时间。

我的优先级是这样的:地面和道路最先做,因为它们决定了整个场景的布局;然后是建筑,因为体积大、视觉占比高;接着是树木,可以用简单的圆柱加球体组合,但要注意树冠的形状要符合像素图的轮廓;最后是道具,这些可以用最简单的几何体,甚至用面片加贴图来替代。

这里有个经验:像素图的元素往往有“夸张”的比例,比如房子可能比树还矮,门可能比窗户还大。建模时不要试图“修正”这些比例,而是要忠实还原,因为这种夸张感正是像素风的魅力所在。我在第一次做的时候把房子拉高到正常比例,结果整个场景的韵味全没了,后来返工重做。

3.3 坐标系统的统一约定

在开始建模之前,必须和团队(或者自己)约定好坐标系统。Blender默认是Z轴向上,Unity是Y轴向上,Godot也是Y轴向上。这意味着从Blender导出到引擎时,会发生轴向转换。如果不提前处理,导入后模型可能会躺在地上或者朝向错误。

我的做法是:在Blender里建模时就用Y轴向上,具体操作是在场景设置里把“Up Axis”改成Y。这样导出glTF时不需要额外旋转,导入Unity和Godot后方向就是对的。另外,所有模型的原点都设置在底部中心,这样摆放时只需要设置XZ坐标,Y坐标直接设为0就能贴地,不用逐个调整高度。

4. Blender建模与材质处理实操

4.1 地面与道路的快速搭建

地面是场景的基础,我一般用一个大的平面(Plane)作为底,然后根据像素图的道路走向,用切割工具(Knife)或者布尔运算挖出道路的形状。更高效的方法是:把像素图导入Blender作为背景参考图,然后沿着道路的边缘描点,用“挤出”命令生成道路面片。

具体操作:在Blender里新建一个平面,缩放到场景尺寸(比如32x32米),然后导入像素图作为Empty的贴图,调整位置让它和平面重合。接着进入编辑模式,用Knife工具沿着道路边缘切割,把道路区域分离出来,单独赋予一个材质。道路材质用纯色,颜色从色板里取,不需要光照,直接用Emission Shader或者把Roughness拉到1、Specular拉到0,这样看起来就是扁平的。

注意:切割时尽量保持四边形面片,避免出现三角形或N-gon,否则后续导出时可能出现法线问题。如果切割后产生了N-gon,用“Triangulate”修改器自动三角化。

4.2 建筑建模:从方块到细节

像素游戏里的建筑通常是简单的几何体组合:一个长方体做主体,一个斜面做屋顶,几个小方块做门窗。我在Blender里的流程是:先建一个立方体,按像素图的比例调整尺寸,然后用Inset和Extrude做出门窗的凹槽,最后用Bevel修改器给边缘加一点倒角,让它在3D视角下不至于太生硬。

屋顶的处理稍微麻烦一点。像素图的屋顶通常是梯形或者三角形,在3D里可以用一个立方体然后缩放顶面来实现,或者用两个斜面拼成一个人字顶。我推荐后者,因为人字顶在引擎里看起来更有立体感。具体做法:建一个平面,旋转45度,然后镜像复制一份,调整位置让两个斜面在顶部相交,最后合并顶点。

材质方面,建筑的外墙用纯色材质,颜色从色板取。门窗可以用深色材质,或者直接用一个面片贴像素图的门窗图案。我试过用面片贴图,效果更接近原图,但要注意面片要稍微突出墙面0.01米,避免Z-fighting。

4.3 植被与道具的批量处理

树木是场景里数量最多的元素,如果一棵棵手动建,效率太低。我的做法是:先建一棵标准树,用圆柱做树干,用球体或圆锥做树冠,调整到符合像素图的比例。然后把这棵树存成一个单独的集合(Collection),用“Linked Duplicate”(Alt+D)复制到场景各处。这样修改一棵树,所有副本都会同步更新,省时省力。

草丛和灌木可以用更简单的方式:一个平面加一个透明贴图,贴图里画好草的轮廓。这种方法在远处看效果很好,近看会穿帮,但像素风场景通常相机距离较远,问题不大。道具比如告示牌、木箱,直接用立方体加贴图就行,不需要太多细节。

实操心得:Blender的“Array”修改器非常适合做栅栏、路灯这类重复排列的物体。设置好偏移量后,一次性生成一排,比手动复制快得多。但要注意,Array修改器生成的对象在导出时会合并成一个Mesh,如果后续需要在引擎里单独控制,记得先Apply修改器再分离。

4.4 材质与色板的映射

这是保持像素风的关键一步。我在Blender里创建材质时,不会用默认的Principled BSDF,而是用Emission节点,把颜色直接连到输出。这样材质不受光照影响,渲染出来就是纯色,和像素图一致。具体节点连接:Emission节点的Color连到Material Output的Surface,Strength设为1。

如果场景需要一点光影变化,可以用“Diffuse BSDF”加一个非常弱的灯光,但我的经验是:像素风场景最好保持无光照,靠颜色本身的明暗来表现层次。比如房子的墙面用浅色,屋顶用深色,这样即使没有光照,也能看出立体感。

色板的映射要精确。我会在Blender里创建一个Color Palette,把像素图的颜色逐个添加进去,然后每个材质直接从Palette里取色。这样保证所有颜色都是原图里的,不会出现“近似色”导致的偏差。

5. Codex辅助生成导入与摆放脚本

5.1 用Codex生成Unity导入脚本

Unity导入FBX或glTF后,默认的材质和缩放可能不符合预期。我让Codex写了一个Editor脚本,在导入时自动设置材质、缩放和碰撞体。核心逻辑是:读取模型的名称,根据名称匹配预设的配置,然后应用。

using UnityEngine; using UnityEditor; public class PixelSceneImporter : AssetPostprocessor { void OnPreprocessModel() { ModelImporter importer = assetImporter as ModelImporter; if (importer == null) return; importer.globalScale = 1.0f; importer.useFileScale = true; importer.importMaterials = true; importer.materialImportMode = ModelImporterMaterialImportMode.ImportStandard; importer.materialLocation = ModelImporterMaterialLocation.External; // 根据模型名称设置碰撞体 string name = importer.assetPath.ToLower(); if (name.Contains("building") || name.Contains("tree")) { importer.addCollider = true; } } }

这段代码的作用是:导入模型时自动设置缩放为1、使用文件缩放、导入材质,并且给建筑和树木自动添加碰撞体。Codex生成初版后,我手动调整了材质导入模式,因为默认的“ImportStandard”在某些情况下会丢失Emission属性,改成“ImportViaMaterialDescription”后颜色才正确。

5.2 场景物体摆放的自动化

手动在Unity里摆放几十个物体太累,我让Codex写了一个脚本,读取一个JSON文件,里面记录了每个物体的名称、坐标和旋转角度,然后自动实例化。

using UnityEngine; using System.IO; using System.Collections.Generic; [System.Serializable] public class SceneObjectData { public string name; public float x; public float y; public float z; public float rotY; } [System.Serializable] public class SceneData { public List<SceneObjectData> objects; } public class SceneBuilder : MonoBehaviour { public TextAsset jsonFile; public GameObject[] prefabs; void Start() { SceneData data = JsonUtility.FromJson<SceneData>(jsonFile.text); foreach (var obj in data.objects) { GameObject prefab = System.Array.Find(prefabs, p => p.name == obj.name); if (prefab != null) { Vector3 pos = new Vector3(obj.x, obj.y, obj.z); Quaternion rot = Quaternion.Euler(0, obj.rotY, 0); Instantiate(prefab, pos, rot, transform); } } } }

JSON文件的格式很简单,每个物体一条记录。这个JSON我是用Python脚本从像素图的坐标表生成的,Codex帮我写了Python端的代码,读取CSV然后输出JSON。整个链路是:像素图分析得到CSV坐标表 -> Python转JSON -> Unity读取JSON自动摆放。这样如果后续调整了像素图,只需要重新生成JSON,场景就能自动更新。

5.3 Godot端的对应实现

Godot用GDScript,逻辑和Unity类似,但API不同。Codex生成的Godot脚本如下:

extends Node3D var scene_data = [] var prefabs = {} func _ready(): load_prefabs() load_scene_data() build_scene() func load_prefabs(): var dir = DirAccess.open("res://prefabs/") if dir: dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": if file_name.ends_with(".tscn"): var prefab = load("res://prefabs/" + file_name) prefabs[file_name.get_basename()] = prefab file_name = dir.get_next() func load_scene_data(): var file = FileAccess.open("res://scene_data.json", FileAccess.READ) var json = JSON.parse_string(file.get_as_text()) scene_data = json["objects"] func build_scene(): for obj in scene_data: var prefab = prefabs.get(obj["name"]) if prefab: var instance = prefab.instantiate() instance.position = Vector3(obj["x"], obj["y"], obj["z"]) instance.rotation_degrees.y = obj["rotY"] add_child(instance)

Godot的版本需要注意一点:FileAccess和DirAccess是Godot 4.x的API,如果你用的是3.x,需要改成File和Directory。Codex默认生成的是4.x版本,我手动改了一版3.x的,这里就不贴了,思路一样。

5.4 Codex使用中的注意事项

用Codex生成代码有几个坑要避开。第一,它有时候会生成不存在的API,比如Unity的ModelImporterMaterialImportMode在旧版本里没有,需要根据你的引擎版本调整。第二,它生成的代码往往缺少错误处理,比如JSON文件不存在时会直接崩溃,需要自己加上try-catch或者null检查。第三,它倾向于生成“完整”的代码,但实际项目中你可能只需要其中一小部分,要学会裁剪。

我的习惯是:把Codex当成一个“高级代码补全”,而不是“自动编程”。先自己想清楚要什么函数、什么参数、什么返回值,然后让Codex填充实现,最后自己审查和测试。这样效率最高,也最不容易出问题。

6. Unity与Godot场景组装与调试

6.1 Unity中的场景配置

在Unity里,我新建一个3D项目(URP或Built-in都行,我用的是URP),然后把Blender导出的glTF文件拖进Assets。导入后,把Prefab拖到场景里,运行SceneBuilder脚本,场景就自动生成了。

相机设置很关键。像素风场景适合用正交相机(Orthographic),这样没有透视变形,看起来更像2D游戏。我把相机的Projection设为Orthographic,Size设为10左右,然后写了一个简单的相机控制器,让相机可以跟随鼠标旋转和缩放。

using UnityEngine; public class CameraController : MonoBehaviour { public float panSpeed = 10f; public float zoomSpeed = 5f; public float minZoom = 5f; public float maxZoom = 20f; private Camera cam; void Start() { cam = GetComponent<Camera>(); } void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); transform.Translate(new Vector3(h, 0, v) * panSpeed * Time.deltaTime, Space.World); float scroll = Input.GetAxis("Mouse ScrollWheel"); cam.orthographicSize -= scroll * zoomSpeed; cam.orthographicSize = Mathf.Clamp(cam.orthographicSize, minZoom, maxZoom); } }

这个控制器很简单,WASD平移,滚轮缩放。如果你想要更复杂的轨道相机,可以用Unity的Cinemachine包,但我觉得对于这个小场景,自己写一个更轻量。

6.2 Godot中的场景配置

Godot的流程类似,但有几个不同点。第一,Godot导入glTF后,材质会自动转换成StandardMaterial3D,但Emission属性可能需要手动开启。第二,Godot的相机也是正交相机,设置projection = PROJECTION_ORTHOGONAL,然后调整size参数。

Godot的相机控制器用GDScript写:

extends Camera3D @export var pan_speed = 10.0 @export var zoom_speed = 5.0 @export var min_zoom = 5.0 @export var max_zoom = 20.0 func _process(delta): var input_dir = Vector3.ZERO if Input.is_action_pressed("ui_left"): input_dir.x -= 1 if Input.is_action_pressed("ui_right"): input_dir.x += 1 if Input.is_action_pressed("ui_up"): input_dir.z -= 1 if Input.is_action_pressed("ui_down"): input_dir.z += 1 translate(input_dir * pan_speed * delta) if Input.is_action_just_pressed("zoom_in"): size -= zoom_speed if Input.is_action_just_pressed("zoom_out"): size += zoom_speed size = clamp(size, min_zoom, max_zoom)

需要在项目设置里添加zoom_in和zoom_out的输入映射,绑定到鼠标滚轮。Godot的输入系统比Unity稍微麻烦一点,但配置一次就好了。

6.3 光照与后处理的取舍

像素风场景要不要加光照?我的答案是:可以加,但要克制。我试过完全无光照,场景看起来太平,缺乏层次;也试过全光照,结果颜色完全偏离原图。最终的方案是:用一个很弱的平行光,强度设为0.3左右,角度调成斜上方45度,然后关闭阴影。这样场景有一点明暗变化,但不会破坏像素风的扁平感。

后处理方面,我加了一个Color Grading,把饱和度稍微提高一点,对比度也提一点,让颜色更鲜艳。但不要加Bloom或Ambient Occlusion,这些效果会让画面变得“糊”,失去像素的锐利感。

注意:Unity的URP和Godot的后处理配置方式不同。Unity需要在Volume里添加Color Adjustments,Godot需要在WorldEnvironment里设置Adjustment。两者的参数名称不一样,但效果类似,需要根据实际画面微调。

6.4 性能优化与导出设置

这个场景的物体数量不多,性能不是大问题,但有几个点还是要注意。第一,把相同材质的物体合并成一个Mesh,减少Draw Call。Unity里可以用Static Batching,Godot里可以用MultiMesh。第二,树木和草丛如果数量多,可以用LOD或者Billboard,远处用面片,近处用模型。第三,导出时关闭不必要的选项,比如Unity的“Optimize Mesh”可以开启,Godot的“Generate Tangents”可以关闭,因为像素风材质不需要法线贴图。

导出设置方面,Blender导出glTF时,我勾选了“Apply Modifiers”和“Export Materials”,不勾“Export Cameras”和“Export Lights”,因为相机和灯光在引擎里重新创建。格式选“glTF Binary (.glb)”,这样所有资源打包成一个文件,导入更方便。

7. 常见问题与排查技巧实录

7.1 模型导入后颜色不对怎么办

这是最常见的问题。原因通常是材质的Emission属性没有正确导入。Unity里检查材质的Shader是不是“Universal Render Pipeline/Unlit”或者“Standard”,如果是Standard,把Emission勾上,颜色设为原色。Godot里检查StandardMaterial3D的“Emission”是否开启,Emission Color是否设为原色。

如果颜色还是偏暗,检查引擎的颜色空间是不是Linear。Unity在Player Settings里,Godot在Project Settings里,把Color Space改成Linear。这个设置对像素风场景影响很大,Gamma空间下颜色会偏亮,Linear空间下更准确。

7.2 模型比例不对怎么排查

导入后模型太大或太小,通常是缩放设置的问题。Blender里检查场景的Unit Scale是不是1.0,导出时检查“Scale”是不是1.0。Unity里检查Model Importer的“Scale Factor”是不是1.0,Godot里检查Import设置的“Scale”是不是1.0。

如果比例差得离谱,比如大了100倍,那可能是单位换算问题。Blender默认单位是米,Unity也是米,Godot也是米,理论上不需要换算。但如果Blender里用了厘米或者英寸,就会出问题。我的建议是:统一用米,所有尺寸都按米来算。

7.3 场景物体位置偏移怎么处理

自动摆放后物体位置不对,通常是坐标系转换的问题。Blender是Z向上,Unity和Godot是Y向上,如果导出时没有转换,物体的Y和Z坐标会互换。解决方法:在Blender导出时勾选“+Y Up”,或者在引擎里手动旋转模型。

另一个可能的原因是原点位置。如果模型的原点不在底部中心,摆放时Y坐标设为0会导致模型一半埋在地下。解决方法:在Blender里把原点设置到几何中心底部,具体操作是进入编辑模式,选择所有顶点,然后“Origin to Geometry”再手动调整Z值。

7.4 常见问题速查表

问题现象可能原因解决方法
模型颜色偏暗颜色空间为Gamma改为Linear
模型颜色偏亮Emission强度过高降低Emission Strength
模型太大/太小缩放设置不一致检查Blender和引擎的Scale
模型位置偏移坐标系未转换导出时勾选+Y Up
模型埋在地下原点不在底部重新设置原点
材质丢失导出时未包含材质勾选Export Materials
场景卡顿Draw Call过高合并Mesh或使用LOD
相机看不到物体相机裁剪面设置不对调整Near/Far Clip

7.5 独家避坑技巧

第一个技巧:在Blender里给每个物体命名时,用英文和数字,不要用中文或特殊字符。Unity和Godot对中文名称的支持不稳定,可能导致导入失败或者脚本找不到物体。

第二个技巧:导出前先保存一个Blender源文件,再导出glTF。这样如果后续需要修改,可以直接改源文件重新导出,不用从头建模。

第三个技巧:在Unity和Godot里分别建一个测试场景,先导入一个简单的立方体,确认材质和缩放都正确,再导入完整的场景。这样可以快速定位问题是出在导出端还是导入端。

第四个技巧:用Git管理项目,每次修改前提交一次。Blender文件、glTF文件、引擎项目文件都纳入版本控制。这样如果改坏了,可以随时回滚。

第五个技巧:如果场景里有透明材质(比如树叶),在Unity里要把材质渲染队列设为“Transparent”,在Godot里把“Transparency”设为“Alpha”。否则透明部分会显示为黑色。

8. 后续扩展与个人体会

这个项目做完之后,我又尝试了几个扩展方向。一个是把场景做成可交互的,比如点击建筑弹出信息面板,这个用Unity的EventSystem或者Godot的Area3D加信号就能实现。另一个是加入简单的角色控制,让一个像素小人可以在场景里走动,这个需要用到NavMesh或者AStar寻路,Codex也能帮忙生成基础代码。

还有一个比较有意思的方向是:用同样的流程还原其他经典像素场景,比如「真新镇」或者「初始之村」。核心流程是一样的,只是像素图不同,坐标表和色板需要重新提取。如果做多了,可以把整个流程脚本化,输入一张像素图,自动输出3D场景,那就真的是一键还原了。

我个人在实际操作中的体会是:2D转3D最难的不是建模,而是“克制”。像素图的魅力在于它的抽象和留白,如果3D化时加了太多细节,反而会失去那种味道。所以我在建模时尽量做减法,能用一个方块表现的就不用两个,能用纯色就不用贴图。最终的效果是:从远处看,场景和原图几乎一样;走近看,又能感受到3D的立体感。这种“似与不似之间”的平衡,是这个项目最有意思的地方。

最后再分享一个小技巧:如果你觉得Blender建模太慢,可以试试用Codex生成Blender的Python脚本,直接通过代码创建几何体。比如用bpy.ops.mesh.primitive_cube_add()创建立方体,然后设置位置和缩放。这种方式适合批量创建规则排列的物体,比如一排栅栏或者一片树林。虽然不如手动建模灵活,但对于简单场景来说,效率能提升好几倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询