引言:
技术美术(Technical Artist)作为游戏开发中的“多面手”,既要懂技术,又要懂艺术,常常需要在两者之间找到平衡。然而,随着项目规模的扩大和需求的增加,技术美术的工作量也在不断增加。幸运的是,AI技术的快速发展为我们提供了新的解决方案。本文将探讨如何利用AI工具提升技术美术的工作效率,并分享一些实际应用案例。
技术美术的核心工作内容:
角色定位:技术美术是程序员和艺术家之间的桥梁,负责优化工作流程、解决技术问题、实现视觉效果。
常见任务:
材质和着色器开发
工具开发与自动化
性能优化
美术资源导入与优化
实时渲染效果实现
痛点分析:
- 重复性任务
编写DCC插件:
在开发DCC(Digital Content Creation,如Maya、Blender、3ds Max)插件时,需要频繁查阅官方API文档,以了解如何调用特定功能(如导入模型、设置材质、导出动画等)。
这个过程非常耗时,尤其是当API文档不够清晰或缺乏示例代码时。
重复性Shader功能:
在开发Shader时,许多功能是重复的(如PBR材质、UV动画、顶点位移等)。每次开发新Shader时,都需要重新编写这些基础功能,浪费大量时间。 - 复杂的技术实现
跨平台兼容性:
在开发Shader或工具时,技术美术需要确保其在不同平台(如PC、移动设备、主机)上的兼容性。
不同平台的渲染管线和性能限制不同,增加了开发和调试的复杂性。
案例:在移动设备上,Shader需要使用低精度计算(half),而在PC上可以使用高精度(float)。
实时渲染效果:
实现复杂的实时渲染效果(如体积光、屏幕空间反射、毛发渲染)需要深入理解图形学原理和渲染管线。
这些效果的实现往往需要大量的调试和优化。 - 时间压力
快速迭代需求:
在游戏开发中,美术效果需要快速迭代,技术美术需要在短时间内实现和优化效果。
这种时间压力可能导致代码质量下降或功能实现不完整。
多任务并行:
技术美术通常需要同时处理多个任务(如Shader开发、工具开发、性能优化),这可能导致注意力分散和效率下降。 - 工具链不完善
缺乏自动化工具:
许多工作流程缺乏自动化工具,导致技术美术需要手动处理大量重复性任务(如资源导入、材质分配、LOD生成)。
工具维护成本高:
自定义工具的开发需要投入大量时间,且随着项目需求的变化,工具需要不断更新和维护。 - 学习曲线陡峭
新技术的学习:
技术美术需要不断学习新的技术和工具(如新的渲染管线、AI工具、DCC插件开发)。
这些技术的学习曲线往往很陡峭,尤其是在缺乏文档或教程的情况下。
跨领域知识:
技术美术需要掌握多个领域的知识(如编程、图形学、美术工具),这可能导致学习压力和工作负担。 - 沟通与协作
与程序员的沟通:
技术美术需要与程序员紧密合作,但在沟通技术细节时可能存在障碍(如术语不一致、需求不明确)。
与美术的沟通:
技术美术需要将美术需求转化为技术实现,但在沟通中可能存在理解偏差。
实际案例分享:开发多光源效果烘焙工具
在最近的一个项目中,我们面临了一个常见的技术挑战:前向渲染中灯光数量过多导致的性能瓶颈。由于前向渲染对灯光数量的限制,场景中动态光源过多会导致Shader中的灯光循环遍历变得极其耗时。为了解决这一问题,我决定开发一个工具,通过烘焙多光源效果来减少Shader中的计算开销。该工具的核心思想是利用Compute Shader实时计算包围盒内的光源信息,并将结果存储到一张3D纹理中,从而实现仅需一次采样即可获取全部灯光信息,无需在Shader中进行逐光源遍历,同时支持动态光源的实时更新。尽管工具的原理并不复杂,但在开发过程中存在大量重复性工作。为了提高效率,我尝试使用JetBrains Rider中的MarsCode AI插件来辅助完成脚本开发。
工具介绍:
MarsCode AI:MarsCode AI 是 JetBrains Rider 中的一个AI编程助手插件,基于GPT技术,能够根据自然语言描述生成代码、提供代码补全建议,并帮助调试代码。
Rider:JetBrains Rider 是一个强大的C# IDE,广泛用于Unity开发。
实现过程:
- 需求分析
问题描述:
场景中有大量动态光源,前向渲染中Shader需要遍历所有光源,导致性能瓶颈。
需要一种方法,将多光源的效果烘焙到一张3D纹理中,Shader只需采样一次即可获得所有光源信息。
工具功能:
实时计算场景中所有光源的贡献,并将结果存储到3D纹理中。
支持动态光源的实时更新。
提供GUI面板,方便美术人员调整参数。 - 使用MarsCode AI生成代码
自然语言描述:
在Rider中,通过MarsCode AI插件输入以下描述:
编写一个Unity工具,名为VolumeLightingSystem的类,通过ComputeShader实时计算包围盒内中所有光源的贡献结果(忽略平行光)需要支持点光源和聚光灯,并将结果存储到3D纹理中。
AI生成的代码框架:
VolumeLightingSystem.cs关键函数如下:
void BakeLights()
{
if (lightBakingComputeShader == null || resultTexture == null)
return;
if(isRealtime == false && isBaked == true)
return;
isBaked = true;
// 设置Compute Shader参数
int kernelHandle = lightBakingComputeShader.FindKernel("CalculateLight");
lightBakingComputeShader.SetTexture(kernelHandle, "_ResultTex", resultTexture);
lightBakingComputeShader.SetVector("_LightVolumeMin", transform.position - size / 2);
lightBakingComputeShader.SetVector("_LightVolumeMax", transform.position + size / 2);
lightBakingComputeShader.SetVector("_TextureSize",
new Vector3(resultTexture.width, resultTexture.height, resultTexture.volumeDepth));
lightPositions.Clear();
lightColors.Clear();
lightIntensities.Clear();
lightRanges.Clear();
lightSpotAnglesAndTypes.Clear();
lightSpotForwards.Clear();
Profiler.BeginSample("GetLightsGC");
Light[] lights = FindObjectsOfType<Light>();
Profiler.EndSample();
Vector3 volumeMin = transform.position - size / 2;
Vector3 volumeMax = transform.position + size / 2;
foreach (var light in lights)
{
if (light.type == LightType.Directional) continue; // 忽略平行光
Vector3 lightPos = light.transform.position;
if (lightPos.x < volumeMin.x || lightPos.x > volumeMax.x ||
lightPos.y < volumeMin.y || lightPos.y > volumeMax.y ||
lightPos.z < volumeMin.z || lightPos.z > volumeMax.z)
{
continue; // 忽略不在包围盒内的光源
}
lightPositions.Add(new Vector4(lightPos.x, lightPos.y, lightPos.z, 1.0f));
lightColors.Add(new Vector4(light.color.r, light.color.g, light.color.b, light.intensity));
lightIntensities.Add(light.intensity);
lightRanges.Add(light.range);
if (light.type == LightType.Spot)
{
float outerAngle = light.spotAngle;
float innerAngle = outerAngle * 0.5f;
lightSpotAnglesAndTypes.Add(new Vector2(outerAngle, innerAngle));
lightSpotForwards.Add(-light.transform.forward);
}
else
{
lightSpotAnglesAndTypes.Add(Vector2.zero);
lightSpotForwards.Add(Vector3.zero);
}
}
// 创建或更新ComputeBuffer
if (lightPositionsBuffer == null || lightPositionsBuffer.count < lightPositions.Count)
{
if(lightPositions.Count == 0)
return;
ReleaseBuffers();
lightPositionsBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float) * 4);
lightColorsBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float) * 4);
lightIntensitiesBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float));
lightRangesBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float));
lightSpotAnglesAndTypesBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float) * 2);
lightSpotForwardsBuffer = new ComputeBuffer(lightPositions.Count, sizeof(float) * 3);
}
lightPositionsBuffer.SetData(lightPositions);
lightColorsBuffer.SetData(lightColors);
lightIntensitiesBuffer.SetData(lightIntensities);
lightRangesBuffer.SetData(lightRanges);
lightSpotAnglesAndTypesBuffer.SetData(lightSpotAnglesAndTypes);
lightSpotForwardsBuffer.SetData(lightSpotForwards);
lightBakingComputeShader.SetInt("_LightCount", lightPositions.Count);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightPositions", lightPositionsBuffer);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightColors", lightColorsBuffer);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightIntensities", lightIntensitiesBuffer);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightRanges", lightRangesBuffer);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightSpotAnglesAndTypes", lightSpotAnglesAndTypesBuffer);
lightBakingComputeShader.SetBuffer(kernelHandle, "_LightSpotForwards", lightSpotForwardsBuffer);
// 执行Compute Shader
int threadGroupsX = Mathf.CeilToInt(resultTexture.width / 8f);
int threadGroupsY = Mathf.CeilToInt(resultTexture.height / 8f);
int threadGroupsZ = Mathf.CeilToInt(resultTexture.volumeDepth / 8f);
m_ComputeCommandBuffer.DispatchCompute(lightBakingComputeShader, kernelHandle,
threadGroupsX,
threadGroupsY,
threadGroupsZ);
Graphics.ExecuteCommandBuffer(m_ComputeCommandBuffer);
m_ComputeCommandBuffer.Clear();
}
CompputeShader如下:
#pragma kernel CalculateLight
RWTexture3D<float4> _ResultTex;
float3 _LightVolumeMin;
float3 _LightVolumeMax;
float3 _TextureSize;
StructuredBuffer<float4> _LightPositions;
StructuredBuffer<float4> _LightColors;
StructuredBuffer<float> _LightIntensities;
StructuredBuffer<float> _LightRanges;
StructuredBuffer<float2> _LightSpotAnglesAndTypes; // x: outer angle, y: inner angle
StructuredBuffer<float3> _LightSpotForwards;
int _LightCount;
float smoothstep(float edge0, float edge1, float x)
{
float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
[numthreads(8, 8, 8)]
void CalculateLight(uint3 id : SV_DispatchThreadID)
{
float3 voxelPos = float3(
lerp(_LightVolumeMin.x, _LightVolumeMax.x, (float)id.x / _TextureSize.x),
lerp(_LightVolumeMin.y, _LightVolumeMax.y, (float)id.y / _TextureSize.y),
lerp(_LightVolumeMin.z, _LightVolumeMax.z, (float)id.z / _TextureSize.z)
);
float4 result = float4(0, 0, 0, 0);
for (int i = 0; i < _LightCount; i++)
{
float3 toLight = _LightPositions[i].xyz - voxelPos;
float sqrDistance = dot(toLight, toLight);
float rangeSqr = _LightRanges[i] * _LightRanges[i];
float attenuation = 1.0f / (1.0f + 25.0f * sqrDistance / rangeSqr);
if (sqrDistance > rangeSqr) continue;
if (_LightSpotAnglesAndTypes[i].y > 0) // 聚光灯
{
float3 lightDir = normalize(toLight);
float spotEffect = dot(lightDir, _LightSpotForwards[i]);
float outerAngleCos = cos(_LightSpotAnglesAndTypes[i].x * 0.5f * 0.017453292f);
float innerAngleCos = cos(_LightSpotAnglesAndTypes[i].y * 0.5f * 0.017453292f);
if (spotEffect > outerAngleCos)
{
float angleFalloff = smoothstep(outerAngleCos, innerAngleCos, spotEffect);
attenuation *= angleFalloff;
result += _LightColors[i] * _LightIntensities[i] * attenuation;
}
}
else // 点光源
{
float distanceFalloff = 1.0f - (sqrDistance / rangeSqr);
attenuation *= distanceFalloff;
result += _LightColors[i] * _LightIntensities[i] * attenuation;
}
}
_ResultTex[id] = float4(result.rgb, 1);
}
3. 反复测试与优化:
反复测试:
当AI生成代码后,我们便进入测试阶段。在这一阶段,我们需要验证生成的功能是否符合预期。如果发现问题,需向AI清晰地描述错误的具体位置以及我们期望的效果。通过反复修改和测试,最终可以得到符合需求的结果
结果验证成功:成功的将光源信息烘焙在3D纹理中
优化:在核心功能验证完成后,我们将进入优化阶段。首先,我会利用AI工具生成一套GUI界面,方便参数调节和功能调试;其次,我会借助AI分析代码结构,识别并优化潜在的性能瓶颈。
自然语言描述:当拓展一个VolumeLightingSystemEditor.cs类,可以再Scene窗口通过包围盒中心上的按钮对包围盒大小进行调节。
AI生成代码框架:
private void OnSceneGUI()
{
var system = target as VolumeLightingSystem;
if (system == null) return;
// 圆形参数
float radius = 1.5f;
Vector3 center = system.transform.position;
// 绘制原始包围盒
Vector3 halfSize = system.size * 0.5f;
Handles.color = Color.white;
Handles.DrawWireCube(center, system.size);
// 绘制尺寸标签
GUIStyle style = new GUIStyle() {
fontSize = 14,
normal = new GUIStyleState() { textColor = Color.white }
};
// 绘制尺寸标签
Handles.Label(center,
$"Size: {Mathf.CeilToInt(system.size.x)}x{Mathf.CeilToInt(system.size.y)}x{Mathf.CeilToInt(system.size.z)}", style);
// 创建6个方向的操作手柄
for (int axis = 0; axis < 3; axis++)
{
Vector3 axisDir = GetAxisDirection(axis);
float handleSize = HandleUtility.GetHandleSize(center) * 0.2f;
// 正方向滑条
Vector3 posPositive = center + axisDir * halfSize[axis];
EditorGUI.BeginChangeCheck();
Vector3 newPosPositive = Handles.Slider(
posPositive,
axisDir,
handleSize,
Handles.ArrowHandleCap,
0.5f);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(system, "Resize Bounds");
float delta = Vector3.Dot(newPosPositive - posPositive, axisDir);
system.size[axis] += delta * 2;
system.size[axis] = Mathf.Max(0.1f, system.size[axis]);
system.InitializeRenderTexture();
}
// 负方向滑条
Vector3 posNegative = center - axisDir * halfSize[axis];
EditorGUI.BeginChangeCheck();
Vector3 newPosNegative = Handles.Slider(
posNegative,
-axisDir,
handleSize,
Handles.ArrowHandleCap,
0.5f);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(system, "Resize Bounds");
float delta = Vector3.Dot(newPosNegative - posNegative, -axisDir);
system.size[axis] += delta * 2;
system.size[axis] = Mathf.Max(0.1f, system.size[axis]);
system.InitializeRenderTexture();
}
}
}
GUI效果展示
4. 总结:
通过使用AI辅助编写代码,我们能够显著提升开发效率,节省大量时间。AI工具不仅可以快速生成代码框架,还能提供优化建议和调试支持,帮助我们更专注于解决核心问题,而非陷入重复性工作中。这种开发方式不仅加快了项目进度,也为技术美术提供了更多探索创新可能性的空间。
未来展望:
随着AI编程助手(如MarsCode AI)的不断发展,技术美术将能够更高效地完成脚本开发和工具制作。未来,AI工具可能会进一步集成到工作流中,帮助TA解决更复杂的技术挑战。
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com