人工智能实战:技术美术如何利用AI工具提升工作效率

引言:

技术美术(Technical Artist)作为游戏开发中的“多面手”,既要懂技术,又要懂艺术,常常需要在两者之间找到平衡。然而,随着项目规模的扩大和需求的增加,技术美术的工作量也在不断增加。幸运的是,AI技术的快速发展为我们提供了新的解决方案。本文将探讨如何利用AI工具提升技术美术的工作效率,并分享一些实际应用案例。

技术美术的核心工作内容:

角色定位:技术美术是程序员和艺术家之间的桥梁,负责优化工作流程、解决技术问题、实现视觉效果。

常见任务

材质和着色器开发

工具开发与自动化

性能优化

美术资源导入与优化

实时渲染效果实现

痛点分析

  1. 重复性任务
    编写DCC插件:
    在开发DCC(Digital Content Creation,如Maya、Blender、3ds Max)插件时,需要频繁查阅官方API文档,以了解如何调用特定功能(如导入模型、设置材质、导出动画等)。
    这个过程非常耗时,尤其是当API文档不够清晰或缺乏示例代码时。
    重复性Shader功能:
    在开发Shader时,许多功能是重复的(如PBR材质、UV动画、顶点位移等)。每次开发新Shader时,都需要重新编写这些基础功能,浪费大量时间。
  2. 复杂的技术实现
    跨平台兼容性:
    在开发Shader或工具时,技术美术需要确保其在不同平台(如PC、移动设备、主机)上的兼容性。
    不同平台的渲染管线和性能限制不同,增加了开发和调试的复杂性。
    案例:在移动设备上,Shader需要使用低精度计算(half),而在PC上可以使用高精度(float)。
    实时渲染效果:
    实现复杂的实时渲染效果(如体积光、屏幕空间反射、毛发渲染)需要深入理解图形学原理和渲染管线。
    这些效果的实现往往需要大量的调试和优化。
  3. 时间压力
    快速迭代需求:
    在游戏开发中,美术效果需要快速迭代,技术美术需要在短时间内实现和优化效果。
    这种时间压力可能导致代码质量下降或功能实现不完整。
    多任务并行:
    技术美术通常需要同时处理多个任务(如Shader开发、工具开发、性能优化),这可能导致注意力分散和效率下降。
  4. 工具链不完善
    缺乏自动化工具:
    许多工作流程缺乏自动化工具,导致技术美术需要手动处理大量重复性任务(如资源导入、材质分配、LOD生成)。
    工具维护成本高:
    自定义工具的开发需要投入大量时间,且随着项目需求的变化,工具需要不断更新和维护。
  5. 学习曲线陡峭
    新技术的学习:
    技术美术需要不断学习新的技术和工具(如新的渲染管线、AI工具、DCC插件开发)。
    这些技术的学习曲线往往很陡峭,尤其是在缺乏文档或教程的情况下。
    跨领域知识:
    技术美术需要掌握多个领域的知识(如编程、图形学、美术工具),这可能导致学习压力和工作负担。
  6. 沟通与协作

与程序员的沟通:

技术美术需要与程序员紧密合作,但在沟通技术细节时可能存在障碍(如术语不一致、需求不明确)。

与美术的沟通:

技术美术需要将美术需求转化为技术实现,但在沟通中可能存在理解偏差。

实际案例分享:开发多光源效果烘焙工具

在最近的一个项目中,我们面临了一个常见的技术挑战:前向渲染中灯光数量过多导致的性能瓶颈。由于前向渲染对灯光数量的限制,场景中动态光源过多会导致Shader中的灯光循环遍历变得极其耗时。为了解决这一问题,我决定开发一个工具,通过烘焙多光源效果来减少Shader中的计算开销。该工具的核心思想是利用Compute Shader实时计算包围盒内的光源信息,并将结果存储到一张3D纹理中,从而实现仅需一次采样即可获取全部灯光信息,无需在Shader中进行逐光源遍历,同时支持动态光源的实时更新。尽管工具的原理并不复杂,但在开发过程中存在大量重复性工作。为了提高效率,我尝试使用JetBrains Rider中的MarsCode AI插件来辅助完成脚本开发。

工具介绍:

MarsCode AI:MarsCode AI 是 JetBrains Rider 中的一个AI编程助手插件,基于GPT技术,能够根据自然语言描述生成代码、提供代码补全建议,并帮助调试代码。

Rider:JetBrains Rider 是一个强大的C# IDE,广泛用于Unity开发。

实现过程:

  1. 需求分析
    问题描述
    场景中有大量动态光源,前向渲染中Shader需要遍历所有光源,导致性能瓶颈。
    需要一种方法,将多光源的效果烘焙到一张3D纹理中,Shader只需采样一次即可获得所有光源信息。
    工具功能
    实时计算场景中所有光源的贡献,并将结果存储到3D纹理中。
    支持动态光源的实时更新。
    提供GUI面板,方便美术人员调整参数。
  2. 使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搜狐畅游引擎部

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值