前言
最近打算学习下特效部分,因为现在我的热饭班长血怒特效很挫,长这样
但是人家应该是这样的
虽然说热饭班长没有Shader撑牌面,但就算加上Shader感觉也比不上原版那么霸气,而且有很多奇怪的贴图我都不知道怎么用,比如
我直接黑人问号脸,所以来学习一下
本文大部分内容都收集整理自realtimevfx 网站,也推荐大家去这个网站学习特效制作,大佬作品和教程非常多!
正文
特效是Textures,Sprites,Meshes,Lights和Shader一起配合而创建的酷炫效果。
在特效制作工作流程中,特效纹理是决定最终特效质量的最重要的其中一环,它的制作通常与游戏的风格有很大的相关性,Jason Keyser’s LoL FX breakdowns 频道有很多非常好的示例
VFX的五大艺术准则
Gameplay
特效的制作要去迎合Gameplay,让Gameplay对于玩家来说更加清晰明确,反馈性更强。这一点在许多文章,视频,PPT都有提及,举个例子,LOL中提莫的蘑菇爆炸的时候要播放特效,老版本的蘑菇爆炸特效和欧米伽小队的爆炸特效完全不一样,一方面是特效的质量,另一方面,也是比较重要的一方面,它使玩家能清晰的看到爆炸的范围,以及特效更加清晰,明确,没有老版本蘑菇杂乱无章的感觉
再比如小炮崔斯塔娜的火箭跳跃,火焰拖尾和落地AOE相对于无特效版本,让施法者和敌人都感觉不会太过突兀,当然了,图三远大于真正伤害范围的特效也是不可取的。
形状
避免使用带有过多细节的照片纹理或视觉效果,因为它们可能会产生不必要的干扰因素,取而代之的是,应当使用软硬线条结合的手绘图来获取更好的效果。
举个比较直观的例子男刀的W技能就因为使用了大量细节的贴图纹理(四角蝴蝶镖)会对其余特效的制作,甚至玩家产生干扰,而潘森的W就因为使用了简单的手绘贴图让其特效更加清晰明确。
在创建用于移动的特效时(例如一个笔直飞行轨迹的火球术),应当为特效加上模糊效果(再不济也得加个拖尾效果)来制造出移动的感觉,如果不这样做的话会感觉很突兀
亮度值
搭配/选择良好的亮度值能让VFX的表现力更上一层楼,应当避免使用0%
或者100%
的亮度,因为其往往会和场景或UI混在一起,让人困惑
举个例子,炸弹人的Q技能就因为在其特效中心位置增加了亮度值显得炸弹更有破坏力
搭配使用高对比度的亮度可以做出能量集聚的效果,但是不应滥用,否则团战时将会让人眼花缭乱(实际上我团战打急眼了就是吓急吧乱按)
颜色
颜色的饱和度和上面提到的亮度规则基本上是通用的
在使用颜色的时候,应当只有一个主色调,并且不应当使用两个几乎相反(对比度非常高)的颜色,因为他们不但不会相互衬托,反而会产生竞争关系,导致观感不好
生命周期
每个特效都应当有其生命周期,另外,在生命周期中配置一些与时间成非线性关系的变量将会很有用
例如下面艾克的大招,左侧幂函数的曲线相对于右侧常规线性曲线造成的效果更有力量感
VFX常用技巧
以下图片皆来自 ShannonBerke
实战
资源准备
理论部分了解的差不多了,来实战吧,但是我的建模水平仅限于在Blender拉个Box,连UV都展不明白,所以还是得向LOL学习
对于LOL的资源获取,推荐大家关注 LoL-Fantome 这个开源组织以及作为作者的两位大佬,提供了几乎所有用来学习LOL的工具(警告,仅供个人学习使用,严禁用于商业用途)
LOL的特效贴图和Mesh可以用 Wad解析工具 导出,但是Mesh格式是sco和scb这种非主流格式,所以需要转换格式,这里我们选择obj格式,至于工具的制作,引用封装 LeagueToolkit 库中的 解析代码 即可
但是直接使用的话会遇到一个问题,那就是工具会默认将一个sco的多个子mesh分别导出obj,例如下面这个sco文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [ObjectBegin] Name= blood_drop_01 //sco名称 CentralPoint= 36.5671 -8.7635 -0.0087 //sco中心点 PivotPoint= 0.0000 0.0000 -0.0000 //sco锚点 Verts= 134 //顶点数 29.7181 -1.7972 -0.0328 30.0716 -1.7988 -2.1521 Faces= 220 //面数 .... 3 56 45 66 pasted__blinn1 0.518236577511 0.939145863056 0.611138164997 0.911541640759 0.517365455627 0.939149141312 //三角面 0 3 66 45 55 pasted__blinn1 0.517365455627 0.939149141312 0.611138164997 0.911541640759 0.605477631092 0.911556601524 //三角面 1 3 78 68 67 pasted__pasted__blinn1 0.495640635490 0.114031434059 0.736957550049 0.605616688728 0.749850034714 0.605569958687 //三角面 2 3 78 69 68 pasted__pasted__blinn1 0.495640635490 0.114031434059 0.700808763504 0.605747699738 0.736957550049 0.605616688728 //三角面 3 ... [ObjectEnd]
就会导出两个obj文件,我将他们两个依次导入Blender,如下图所示
这显然不太行,我们要的是一个完整的血滴,当然了,保不齐我们也会有将特效的SubMeshes分开导出obj的需求,所以我准备保留导出多obj的基础上增加SubMesh的合并功能
要修改的话其实也比较简单,修改其源码即可
先来分析下其解析过程,因为涉及多个SubMesh的分离,所以会将顶点索引规范到[0,N]
之间
1 2 3 4 5 for (int i = 0 ; i < indices.Count; i++){ indices[i] -= minVertex; }
为了最小化对源码的入侵,选择将这个minVertex(最小的顶点索引值)存到StaticObjectSubmesh类中,然后在进行SubMeshes合并的时候还原一下即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for (int i = 0 ; i < indices.Count; i++){ indices[i] -= minVertex; } submeshes.Add(new StaticObjectSubmesh(mappedSubmesh.Key, submeshVertices, indices, minVertex)); public uint MinVertexIndex { get ; private set ; }public StaticObjectSubmesh (string name, List<StaticObjectVertex> vertices, List<uint> indices, uint minVertexIndex = 0 ) { this .Name = name; this .Vertices = vertices; this .Indices = indices; this .MinVertexIndex = minVertexIndex; }
导出的完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 using System;using System.Collections.Generic;using LeagueToolkit.IO.StaticObjectFile;using System.IO;using System.Numerics;using LeagueToolkit.IO.OBJ;using LeagueAnimation = LeagueToolkit.IO.AnimationFile.Animation;namespace LeagueToolkit.Sandbox { class Program { private static string s_StaticObjectsPath = "E:/LOLExportAll/Darius/assets/characters/darius/skins/base/particles" ; private static string s_ObjOutPath = $"{s_StaticObjectsPath} /ObjOutPut" ; static void Main (string [] args ) { ExportStatic2Obj(); } static void ExportStatic2Obj (bool exportSubMesh = false ) { if (!Directory.Exists(s_StaticObjectsPath)) { throw new Exception($"{s_StaticObjectsPath} 不存在,请检查路径!" ); } Directory.CreateDirectory(s_ObjOutPath); var staticFilesPaths = Directory.GetFiles(s_StaticObjectsPath); foreach (var staticFilePath in staticFilesPaths) { StaticObject sco = null ; if (staticFilePath.EndsWith(".sco" )) { sco = StaticObject.ReadSCO(staticFilePath); } else if (staticFilePath.EndsWith(".scb" )) { sco = StaticObject.ReadSCB(staticFilePath); } else { continue ; } var name = Path.GetFileNameWithoutExtension(staticFilePath); if (exportSubMesh) { var resultObjs = sco.ToObj(); foreach (var obj in resultObjs) { obj.Obj.Write($"{s_ObjOutPath} /{obj.MaterialName} {name} .obj" ); Console.WriteLine($"导出{s_ObjOutPath} /{obj.MaterialName} {name} .obj 完成" ); } } else { (string MaterialName, OBJFile Obj) finalObjTuple = sco.ToObjWithCombineSubMesh(); finalObjTuple.Obj.Write($"{s_ObjOutPath} /{finalObjTuple.MaterialName} {name} .obj" ); Console.WriteLine($"导出{s_ObjOutPath} /{finalObjTuple.MaterialName} {name} .obj 完成" ); } } } } public static class ObjFileExtension { public static (string MaterialName, OBJFile Obj ) ToObjWithCombineSubMesh (this StaticObject self ) { if (self.Submeshes.Count > 1 ) { List<Vector3> finalVertexs = new List<Vector3>(16 ); List<Vector2> finalUVs = new List<Vector2>(16 ); List<uint> finalIndexs = new List<uint>(16 ); for (int i = 0 ; i < self.Submeshes.Count; i++) { foreach (var staticObjectVertexs in self.Submeshes[i].Vertices) { finalVertexs.Add(staticObjectVertexs.Position); finalUVs.Add(staticObjectVertexs.UV); } foreach (var index in self.Submeshes[i].Indices) { finalIndexs.Add(index + self.Submeshes[i].MinVertexIndex); } } return (self.Submeshes[0 ].Name, new OBJFile(finalVertexs, finalIndexs, finalUVs)); } else { return self.ToObj()[0 ]; } } } }
特效分析
主要有三块
类似边缘光的淡红色特效
烟状物
圆球轨迹环绕的飞絮(分许中心高亮)
类似边缘光的效果
使用两个Shader,或者两个Pass都可以,思路就是常规的边缘光思路,再加上根据Time的偏移和渐隐即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 Shader "NKGMoba/Darius_R_RimLightOffset" { Properties { _MainTex("MainTex",2D)="white"{} _OffsetDir("OffsetDir", Vector) = (0,0,0,0) _OffsetIntensity("OffsetIntensity", Float) = 1 _TimeScale("TimeScale", Float) = 1 } SubShader { Tags {"RenderPipeline" = "UniversalPipeline" "Queue" = "Transparent" "RenderType" = "Transparent"} Cull Off Pass { Tags {"LightMode" = "UniversalForward"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _MainTex_TexelSize; float3 _OffsetDir; float _OffsetIntensity; float _TimeScale; CBUFFER_END TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct Varyings { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal: TEXCOORD1; float3 worldViewDir: TEXCOORD2; }; Varyings vert(Attributes input) { Varyings output; input.positionOS.xyz += _OffsetDir * _OffsetIntensity * (_Time.y % _TimeScale); VertexPositionInputs vertexPositionInput = GetVertexPositionInputs(input.positionOS); output.vertex = vertexPositionInput.positionCS; output.uv = input.uv; output.worldNormal = mul(input.normal, (float3x3)unity_WorldToObject); output.worldViewDir = _WorldSpaceCameraPos.xyz - vertexPositionInput.positionWS; return output; } half4 frag(Varyings input) : SV_Target { float3 worldNormal = normalize (input.worldNormal); float3 worldViewDir = normalize (input.worldViewDir); float rim = max (0 , dot (worldViewDir, worldNormal)); half4 rimColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, float2(clamp (rim, 0.1 f, 0.3 f), 0.5 f)); rimColor.a -= (_Time.y % _TimeScale); return rimColor; } ENDHLSL } } }
烟状物&&絮状物
期间遇到了一个问题:URP管线中因为Shader的keyword定义问题,没有办法完全适配Particles System,例如这个特效的颜色在生命周期中的变化(Color in Lifetime) URP and particles color over lifetime not working
Had to switch to either Universal Render Pipeline/2D/Sprite-Lit-Default or Universal Render Pipeline/Particles/Unlit to get the module to work.
想了想,改是好改,但是这个Particles System未来可能会停止维护,现在主要是Visual Effect Graph使用了Compute Shader做GPU加速运算,但是仍旧有一部分的机型不支持Compute Shader,或者说支持的不是很好,但是不妨碍我们先进行学习,不得不说使用体验极佳,仅用了下图这么点内容就完成了两个特效的制作
期间也遇到了一些小问题,Google一下基本上都有答案,推荐体验嗷
结果
(味道差好多,以后慢慢调了)
引用
网站分享
Realtimevfx-一个高质量VFX学习交流社区
Sketchfab-一个免费模型交流学习网站
教程/资源分享
VFX入门教程精华版
创造VFX所需要遵守的原则
LOL VFX制作知识分享
LOL 召唤师峡谷,您可以出于学习的目的下载它
LOL VFX制作视频合集
LOL VFX教程与作品合集贴
LoL-Fantome