核雕这门手艺,讲究的是"方寸之间见天地"。一颗橄榄核不过拇指大小,匠人却能在上面刻出十八罗汉、赤壁夜游、园林楼阁。但问题也来了——核雕作品体积小、细节密,线下展览时观众得凑到玻璃柜跟前眯着眼看,光线稍差就什么都看不清;一件精品雕了三个月,展览时观众平均停留时间不到四十秒。我去年帮一个非遗工作室做数字化项目时,就碰到了这个矛盾:怎么让观众真正"看进去"核雕的细节,而不是走马观花拍张照就走。
这个项目最后落地的方案,是用Unity 3D加C#做了一套核雕文化主题的虚拟展馆交互漫游系统。观众可以在虚拟展厅里自由行走,走到展柜前触发交互,核雕作品会自动旋转、放大,配合局部高亮和工艺解说,把肉眼看不到的刀工细节一层层剥开。整套系统最终通过WebGL打包,浏览器打开就能用,不需要装任何客户端。下面我把从场景搭建到交互逻辑再到WebGL发布这条链路上的关键决策和踩过的坑,完整梳理一遍。
1. 为什么核雕展馆非要做成虚拟漫游
1.1 核雕展陈的三个死结
先说说传统核雕展览绕不过去的三个问题,这直接决定了技术方案该怎么选。
第一个是尺度矛盾。核雕作品通常在1.5到3厘米之间,最精细的《核舟记》风格作品,窗户能开合、船桨能活动,这些细节在实物展览中几乎无法呈现。观众站在展柜前,视线距离至少30厘米,加上玻璃反光和展厅灯光,实际能看清的细节不到作品本身的四成。我实测过,用手机微距模式拍核雕,对焦成功率不到一半,因为景深太浅,稍微手抖就糊了。
第二个是叙事断裂。核雕不只是雕刻,它背后有题材故事、刀法流派、材料知识。比如同样是罗汉题材,须派罗汉和殷派罗汉的刀法完全不同,一个圆润一个凌厉。但实物展览只能放个说明牌,观众看完雕刻再看文字,中间的理解链条是断的。虚拟展馆可以把解说、动画、局部特写和作品本体绑在一起,观众点哪里讲哪里。
第三个是传播半径。核雕工作室大多在苏州、潍坊、广州这些地方,线下展览覆盖的人群有限。做成WebGL之后,一个链接发出去,手机、平板、电脑都能打开,这对非遗文化的传播价值是数量级的提升。
1.2 为什么选Unity 3D而不是Three.js或Cesium
这里要解释一个关键选型。做Web端3D展示,常见方案有三个:Three.js、Cesium、Unity WebGL。
Three.js轻量、灵活,适合做产品展示类的简单场景,但它的光照系统和材质系统相对基础,要做核雕这种需要精细PBR材质和复杂光照的效果,开发量会非常大。Cesium是地理信息领域的渲染引擎,强项是地形和高程数据,做室内展馆属于杀鸡用牛刀,而且它的交互体系不适合第一人称漫游。
Unity的优势在于:编辑器可视化搭建场景,美术资源导入流程成熟,PBR材质开箱即用,C#的组件化开发模式让交互逻辑清晰可维护。更重要的是,Unity的WebGL导出经过多年迭代,在主流浏览器上的兼容性已经相当稳定。对于这个项目,我需要的是"精细模型+复杂光照+丰富交互",Unity是综合成本最低的选择。
提示:Unity WebGL导出后包体较大,首次加载需要做进度条和资源分块加载,这一点后面会详细讲。
1.3 目标平台决定了技术细节
项目定位是"浏览器打开即用",所以最终发布平台锁定WebGL。这个决定影响了很多后续选择:不能用需要服务端物理模拟的方案,不能用依赖本地文件系统的资源加载方式,光照贴图要提前烘焙,模型面数要严格控制。如果一开始没想清楚发布平台,做到一半发现WebGL跑不动,返工成本极高。
2. 展馆场景搭建:从白模到可漫游空间
2.1 展厅布局的空间逻辑
核雕展馆的空间设计不能随便摆几个柜子就完事。我参考了实际展厅的动线设计,采用"序厅—主展区—工艺区—尾厅"的四段式布局。
序厅放核雕文化的历史沿革,用图文展板加一件大型核雕的旋转展示做视觉焦点。主展区是核心,按题材分四个展岛:罗汉、园林、舟船、花鸟,每个展岛中央放展柜,展柜里是核雕作品。工艺区展示雕刻工具和流程,用动画演示从选核到成品的步骤。尾厅做互动留言和作品合影。
这个布局的关键是动线要顺。我用Unity的NavMesh做了导航网格,观众点击地面任意位置,角色会自动寻路过去,不会穿墙也不会卡在展柜角落。NavMesh的烘焙参数里,Agent Radius设成0.3米(模拟人的肩宽),Agent Height设成1.7米,这样角色不会钻进比人还窄的缝隙。
2.2 核雕模型的精度控制
核雕模型是这个项目的核心资产。原始扫描模型面数动辄几十万面,直接放进WebGL场景,浏览器直接卡死。我的处理流程是:
- 高模扫描:用结构光扫描仪获取核雕的高精度网格,面数在50万到80万之间。
- 拓扑减面:在Blender里用Decimate修改器减到1.5万到3万面,保留轮廓和主要细节。
- 法线烘焙:把高模的细节烘焙到低模的法线贴图上,这样低模也能呈现刀痕的凹凸感。
- 材质制作:核雕的材质是典型的次表面散射效果,橄榄核本身有油脂感,光线打上去会有柔和的透光。我用Unity的Standard Shader配合法线贴图和AO贴图来模拟,金属度设0,光滑度控制在0.3到0.4之间。
这里有个经验:核雕的刀痕方向是有讲究的,不同流派的运刀方向不同,法线贴图如果方向错了,懂行的观众一眼就能看出来。我在烘焙法线时,特意让美术对照实物照片确认了刀痕走向。
2.3 光照烘焙与性能平衡
WebGL平台对实时光照的支持有限,所以展厅的主要光照必须烘焙。我用的是Unity的Progressive Lightmapper,设置如下:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Lightmap Resolution | 20 texels/unit | 展柜区域提高到40 |
| Lightmap Padding | 4 | 避免贴图边缘渗色 |
| Direct Samples | 64 | 直接光采样 |
| Indirect Samples | 512 | 间接光采样 |
| Bounces | 2 | 反弹次数 |
展柜内部的照明单独处理,用自发光材质模拟射灯效果,配合Light Probe给核雕模型提供环境光照。这样做的原因是展柜玻璃会反射环境,如果只用烘焙光照,玻璃后面的核雕会显得很平。
注意:光照烘焙一次要跑二十分钟左右,改一次场景就要重烘。建议先把所有静态物体标记为Static,确认布局不再变动后再烘焙。
3. C#交互逻辑:让观众真正"上手"核雕
3.1 第一人称漫游控制器的实现
漫游控制器是整个交互的基础。我没有用Unity自带的Character Controller,而是自己写了一套,原因是自带组件在WebGL下的输入响应有延迟,而且不支持移动端的触摸摇杆。
核心代码结构是这样的:
public class VisitorController : MonoBehaviour { public float moveSpeed = 3.0f; public float rotateSpeed = 2.0f; public float gravity = -9.81f; private CharacterController controller; private Vector3 velocity; private Transform cameraTransform; void Update() { // 键盘输入(PC端) float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); // 移动端虚拟摇杆输入 if (VirtualJoystick.Instance != null) { h = VirtualJoystick.Instance.Horizontal; v = VirtualJoystick.Instance.Vertical; } Vector3 move = transform.right * h + transform.forward * v; controller.Move(move * moveSpeed * Time.deltaTime); // 重力处理 if (controller.isGrounded && velocity.y < 0) velocity.y = -2f; velocity.y += gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }这里有个细节:移动速度不能设太快。展厅空间有限,速度超过4米/秒观众会晕,而且来不及看清展品。我实测3米/秒是最舒服的,相当于正常步行的速度。
3.2 展品交互的触发机制
观众走到展柜前,怎么触发交互?我试过三种方案:
第一种是碰撞体触发,角色进入展柜前方的触发器范围,自动弹出交互提示。问题是观众可能只是路过,不想看这个展品,弹窗会打断漫游节奏。
第二种是射线检测,从摄像机发射射线,打到展品上才显示提示。这个方案精准,但需要观众主动对准展品,操作成本高。
第三种是距离+朝向双重判定,角色距离展品小于2米,且摄像机朝向与展品夹角小于45度时,才显示交互提示。这个方案最自然,最终采用了。
void CheckExhibitInteraction() { foreach (var exhibit in exhibits) { float dist = Vector3.Distance(transform.position, exhibit.position); if (dist > 2.0f) continue; Vector3 dirToExhibit = (exhibit.position - transform.position).normalized; float angle = Vector3.Angle(cameraTransform.forward, dirToExhibit); if (angle < 45f) { exhibit.ShowInteractionHint(); currentExhibit = exhibit; break; } } }3.3 核雕细节放大的交互设计
这是整个项目最有价值的部分。观众点击展品后,核雕会从展柜中"飞"到观众面前,放大到占据视野的三分之一,然后自动缓慢旋转。同时右侧弹出信息面板,显示作品名称、作者、题材、刀法流派。
更关键的是局部高亮功能。核雕的精华在细节,比如罗汉的衣纹、舟船的窗户。我在模型上预设了若干"兴趣点",观众点击兴趣点,摄像机会平滑移动到该位置的特写视角,同时该区域高亮,其他部分变暗。
public IEnumerator FocusOnDetail(Transform detailPoint, float duration) { Vector3 startPos = cameraTransform.position; Quaternion startRot = cameraTransform.rotation; Vector3 targetPos = detailPoint.position - detailPoint.forward * 0.15f; Quaternion targetRot = Quaternion.LookRotation(detailPoint.forward); float elapsed = 0; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.SmoothStep(0, 1, elapsed / duration); cameraTransform.position = Vector3.Lerp(startPos, targetPos, t); cameraTransform.rotation = Quaternion.Slerp(startRot, targetRot, t); yield return null; } }提示:特写视角的距离要控制好,太近会穿模,太远看不清细节。我实测0.15米是最佳距离,配合摄像机的近裁剪面设为0.05。
3.4 UGUI信息面板的布局与适配
信息面板用UGUI搭建,这里踩过一个坑:Canvas Scaler的适配模式。一开始用Constant Pixel Size,在4K屏幕上文字小得看不清,在手机上又大得溢出。后来改成Scale With Screen Size,参考分辨率设为1920×1080,Match设为0.5(宽高各占一半权重),这样在各种屏幕上表现都正常。
面板的显示和隐藏用Canvas Group的alpha过渡,配合DoTween做淡入淡出,比直接SetActive更平滑。面板内容用Scroll Rect包裹,因为有些核雕作品的解说文字比较长,需要滚动查看。
4. WebGL发布:从编辑器到浏览器的最后一公里
4.1 打包前的性能优化清单
WebGL打包前必须做一轮性能审查,我整理了一份检查清单:
| 检查项 | 标准 | 处理方式 |
|---|---|---|
| 模型面数 | 单件<3万面 | 减面+法线烘焙 |
| 贴图尺寸 | 单张<1024 | 压缩为ASTC或ETC2 |
| Draw Call | <200 | 静态合批+GPU Instancing |
| 光照贴图 | 单张<2048 | 分区域烘焙 |
| 音频格式 | .ogg | 避免.wav |
| 代码裁剪 | 开启 | 移除未使用的引擎模块 |
Draw Call是最容易超标的一项。展厅里的展柜、展板、装饰物如果各自独立渲染,很容易超过300。我的做法是把所有静态物体标记为Static,开启Static Batching,同时把相同材质的展柜合并成一个Mesh。
4.2 资源加载与进度条
WebGL首次加载需要下载整个包体,如果包体有50MB,在普通网络下要等十几秒。如果不做进度条,观众会以为页面卡死了。
Unity的WebGL模板可以自定义,我在模板的HTML里加了一个加载进度条,通过UnityInstance的进度回调更新:
createUnityInstance(canvas, config, (progress) => { progressBar.style.width = (progress * 100) + '%'; progressText.innerText = Math.round(progress * 100) + '%'; }).then((unityInstance) => { loadingScreen.style.display = 'none'; });更进一步,我把核雕模型资源做成了AssetBundle,分展岛加载。观众进入序厅时只加载序厅资源,走到主展区才加载对应展岛的模型。这样首屏加载时间从15秒降到了5秒以内。
4.3 浏览器兼容性实测
我在Chrome、Edge、Firefox、Safari上做了兼容性测试,结果如下:
| 浏览器 | 版本 | 表现 | 问题 |
|---|---|---|---|
| Chrome | 120+ | 流畅 | 无 |
| Edge | 120+ | 流畅 | 无 |
| Firefox | 121+ | 流畅 | 音频需用户交互后播放 |
| Safari | 17+ | 基本流畅 | WebGL 2.0支持不完整,部分Shader报错 |
Safari的问题最麻烦,它对WebGL 2.0的支持是部分实现,一些高级Shader特性用不了。解决方案是在Unity的Player Settings里把WebGL 2.0关掉,强制用WebGL 1.0,牺牲一点画质换兼容性。另外Safari对音频自动播放限制严格,所有音频必须在用户点击后才能播放,这个在交互设计时要提前考虑。
4.4 移动端的触摸适配
移动端没有键盘鼠标,所有操作都要靠触摸。我做了三套输入方案:
- 虚拟摇杆:左下角,控制移动
- 滑动转向:屏幕右侧滑动,控制视角旋转
- 点击交互:点击展品触发交互
虚拟摇杆用UGUI的Event Trigger实现,摇杆背景和摇杆头都是Image,通过监听PointerDown、Drag、PointerUp事件计算偏移量。这里有个细节:摇杆的死区要设大一点,0.15左右,否则手指轻微抖动就会导致角色移动。
5. 踩坑实录:那些文档里不会写的问题
5.1 核雕模型的Z-Fighting问题
核雕的细节非常密集,刀痕之间的间距可能只有0.1毫米。在3D模型里,如果两个面靠得太近,就会出现Z-Fighting,表现为闪烁的条纹。我一开始以为是显卡问题,排查了半天才发现是模型本身的问题。
解决方案有两个:一是拉开面与面之间的距离,在建模时就把刀痕的深度做大一点,虽然和实物有偏差,但视觉上更清晰;二是调整摄像机的近裁剪面,从默认的0.3改成0.05,增加深度缓冲的精度。两个方案结合使用,Z-Fighting基本消失。
5.2 WebGL下的中文乱码
UGUI的Text组件在WebGL下显示中文时,如果字体资源没有正确打包,会出现方块乱码。原因是Unity默认的Arial字体不包含中文字形。解决方案是导入一个包含中文字符的TTF字体,在Font Asset里设置Character Set为Custom,把常用的三千多个汉字加进去。注意不要用Dynamic模式,WebGL下动态字体加载会有问题。
5.3 光照贴图在WebGL下的色差
烘焙好的光照贴图在编辑器里看是正常的,打包到WebGL后整体偏暗。排查后发现是色彩空间的问题。Unity的Color Space默认是Gamma,而WebGL平台推荐用Linear。改成Linear后,光照贴图的显示就正常了。但要注意,改成Linear后所有材质的光照表现都会变化,需要重新调整。
注意:Color Space一旦确定就不要中途更改,否则所有烘焙的光照贴图都要重做。
5.4 移动端的内存溢出
在低端安卓机上测试时,页面加载到一半就崩溃了。用Chrome的Remote Debugging排查,发现是内存溢出。WebGL在移动端的内存上限通常是256MB到512MB,而我的场景加载了太多高清贴图。
解决过程分三步:第一,把所有贴图的压缩格式从RGBA32改成ASTC 6x6,内存占用直接降了四分之三;第二,把不重要的贴图分辨率从2048降到1024;第三,用Resources.UnloadUnusedAssets()在场景切换时主动释放内存。三步做完,低端机也能稳定运行了。
6. 交互细节的打磨:让漫游更像"逛展"
6.1 展柜玻璃的反射效果
展柜玻璃如果只是透明材质,看起来会很假。我用了两层处理:底层是透明材质,上层叠加一个Cubemap反射,反射强度控制在0.3左右。这样玻璃既有通透感,又有环境反射,观众能感觉到"隔着一层玻璃在看"。
Cubemap的来源是展厅的全景截图,用Camera.RenderToCubemap生成。注意这个操作要在光照烘焙完成后做,否则反射出来的环境是错的。
6.2 环境音效的空间化
展厅里加了背景音乐和环境音效,用的是Unity的Audio Source Spatial Blend。观众走到不同区域,听到的音效不同:序厅是古琴曲,主展区是轻微的展厅环境音,工艺区是刻刀雕刻的声音。
空间化音效的关键是Audio Listener要挂在摄像机上,Audio Source的Spatial Blend设为1.0(完全3D),Min Distance和Max Distance根据展厅尺寸调整。我设的是Min 1米、Max 15米,这样观众走近音源时声音会自然变大。
6.3 漫游引导与防迷路设计
展厅面积大了之后,观众容易迷路。我加了两个引导设计:一是地面导引线,用发光的线条指示参观方向;二是小地图,右上角显示展厅俯视图和观众当前位置。
小地图的实现方式是:用一台正交摄像机从上方俯拍展厅,渲染到Render Texture,再把Render Texture显示在UGUI的RawImage上。观众的位置用一个图标表示,每帧更新图标的RectTransform位置。
void UpdateMinimapIcon() { Vector3 worldPos = player.position; Vector2 mapPos = new Vector2( worldPos.x / mapWidth * minimapSize, worldPos.z / mapDepth * minimapSize ); iconRect.anchoredPosition = mapPos; }6.4 作品合影功能的实现
尾厅的合影功能是观众反馈最好的一个点。观众可以调整虚拟角色的姿势,和核雕作品合影,然后保存为图片。
实现方式是:在场景里放一个Render Texture摄像机,对准角色和展品,把Render Texture的内容读取为Texture2D,再编码为PNG下载。这里要注意WebGL下不能直接用File.WriteAllBytes,要用JavaScript的下载接口:
[DllImport("__Internal")] private static extern void DownloadImage(string base64, string filename); public void SavePhoto() { Texture2D tex = new Texture2D(width, height, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, width, height), 0, 0); tex.Apply(); byte[] bytes = tex.EncodeToPNG(); string base64 = Convert.ToBase64String(bytes); DownloadImage(base64, "核雕合影.png"); }7. 项目复盘:几个值得记住的经验
7.1 美术资源规范要提前定
这个项目最大的返工来自美术资源。一开始没有定贴图尺寸和模型面数的规范,美术给过来的模型有的5万面有的20万面,贴图有的4096有的512,整合时花了大量时间做统一处理。如果重来一次,我会在项目启动时就出一份资源规范文档,明确面数上限、贴图尺寸、命名规则、坐标轴朝向。
7.2 交互逻辑要尽早做原型
展品交互的逻辑我改了四版才定下来。如果一开始就用最终模型做测试,每次改逻辑都要重新导入模型,效率很低。正确做法是用简单的Cube和Sphere做交互原型,逻辑跑通了再替换成正式模型。
7.3 WebGL的性能天花板要心里有数
WebGL再优化,性能也比不上原生应用。这个项目的场景里同时显示的核雕模型不超过5件,Draw Call控制在150以内,才能保证中端手机流畅运行。如果要做更复杂的场景,可能需要考虑分场景加载或者降低画质。
7.4 测试要覆盖真实设备
在编辑器里跑得再流畅,不代表真机上没问题。我在项目后期借了五台不同档次的手机做测试,从旗舰机到千元机都跑了一遍,才发现低端机的内存问题。建议在项目中期就开始真机测试,不要等到最后。
这套系统上线后,工作室的线上展览访问量比线下展览的参观人数多了两个数量级,观众平均停留时间从四十秒提升到了六分钟。核雕这种极度依赖细节的手艺,通过虚拟展馆确实找到了新的呈现方式。如果你也在做类似的文化数字化项目,希望这些经验能帮你少走一些弯路。