【Unity3D】花瓣特效

1 花瓣绘制原理

​ 如下图是实现的花瓣特效效果,为方便描述,我们将每个红色的扁状长条称为花瓣,每个花瓣中心的绿点称为花蕊,花朵的正中心称为花心。

​ 我们在 xOz 平面上绘制花朵,假设花心为 O 点,其世界坐标为 _Center, 花瓣个数为 _PetalNum,花瓣半长度和半宽度分别为 _PetalLength、_PetalWidth,背景、花心、花蕊、花瓣的颜色分别为 _BackgoundColor、_HeartColor、_StamenColor、_PetalColor;对于平面上任意一点 P, 其世界坐标为 worldPos,其最终着色的颜色为 color,下面将逐步求解 color 的计算过程。

​ 本文完整资源见→Unity3D花瓣特效

1.1 花瓣坐标轴上投影坐标计算过程

​ 为方便顶点着色,我们需要知道与顶点 P 最近的花蕊坐标,将该花蕊记为 S 点,其坐标记为 stamen,然后再计算 SP 在该花瓣坐标轴上的投影坐标(即图中 SP 在 SM 和 SN 方向上的投影)。

1)计算 OS 的旋转角度

​ 由于 Unity 世界坐标系是左手坐标系,旋转正方向的定义遵循左手法则(详见→空间和变换),即 xOz 平面上旋转正方向为顺时针方向。为简化计算,我们定义旋转零度方向为 z 轴正方向。因此,向量 OS 的旋转角度计算如下。

cpp 复制代码
float3 vertVec = worldPos - _Center; // 向量OP
float proj = dot(normalize(vertVec), float3(0, 0, 1)); // OP在OZ上的投影
float angle = acos(proj); // 向量OP的角度
if (vertVec.x < 0) { // OP的旋转角度大于180°
	angle = UNITY_TWO_PI - angle;
}
float delta = UNITY_TWO_PI / _PetalNum; // 每个花瓣的角度
float nearAngle = round(angle / delta) * delta; // 向量OS的角度

2)计算 S 点坐标

cpp 复制代码
float3 vec = float3(sin(nearAngle), 0, cos(nearAngle)); // 向量OS的单位方向向量
float3 stamen = _Cenetr + vec * _PetalLength; // S点坐标(离P点最近的花蕊坐标)

3)计算 SP 在花瓣坐标轴上的投影坐标

cpp 复制代码
float vec1 = worldPos - stamen; // 向量SP
float vec2 = normalize(stamen - _Center); // 向量SM的单位方向向量
float x = abs(dot(vec1, vec2)); // SP在SM轴上的投影
float y = sqrt(dot(vec1, vec1) - x * x); // SP在SN轴上的投影
float2 proj = float2(x, y); // SP在花瓣坐标轴上的投影坐标

​ 由于花瓣具有对称性,为方便计算,我们只取投影的绝对值。

1.2 顶点着色过程

​ 为了使花瓣、花蕊、花心边缘着色平滑,我们使用了 smoothstep 函数(详见→Shader常量、变量、结构体、函数)。

1)花瓣着色

cpp 复制代码
float rate1 = smoothstep(_PetalLength, 0, proj.x) * smoothstep(_PetalWidth, 0, proj.y); // 顶点属于花瓣的比例
fixed4 color1 = lerp(_BackgroundColor, _PetalColor, rate1); // 混合花瓣颜色

2)花蕊着色

cpp 复制代码
float stamenWidth = _PetalWidth * 0.25; // 花蕊宽度(花蕊占花瓣宽度的比例可以调整)
float len2 = length(worldPos - stamen); // 向量SP的模长(顶点离花蕊的长度)
float rate2 = smoothstep(stamenWidth, 0, len2); // 顶点属于花蕊的比例
fixed4 color2 = lerp(color1, _StamenColor, rate2); // 混合花蕊颜色

3)花心着色

cpp 复制代码
float heartWidth = _PetalLength * 0.4; // 花心宽度(花心占花瓣长度的比例可以调整)
float len3 = length(worldPos - _Center); // 向量OP的模长(顶点离花心的长度)
float rate3 = smoothstep(heartWidth, 0, len3); // 顶点属于花蕊的比例
fixed4 color = lerp(color2, _HeartColor, rate3); // 混合花心颜色

2 绽放花瓣特效

2.1 花瓣绽放原理

​ 调整花瓣的长度和宽度,使其随时间同步周期性变化,从而实现花瓣绽放效果,如下。

cpp 复制代码
float time = _Time.y * _Speed;
_PetalWidth = fmod(time * 0.2, 0.9) + 0.1;
_PetalLength = fmod(time * 1.1111, 5) + 1;

​ 其中 _Speed 为花瓣长度、宽度的变化速度,外部可以调整该参数。

2.2 花瓣绽放实现

​ FlowerEffect.shader

cpp 复制代码
Shader "MyShader/FlowerEffect"  { // 绽放花瓣特效
    Properties{
        _BackgroundColor("BackgroundColor", Color) = (1, 1, 1, 1) // 地面颜色
        _PetalColor("PetalColor", Color) = (1, 0, 0, 1) // 花瓣颜色
        _StamenColor("StamenColor", Color) = (0, 1, 0, 1) // 花蕊颜色
        _HeartColor("_HeartColor", Color) = (1, 1, 0, 1) // 花心颜色
        _Center("Center", Vector) = (0, 0, 0, 0) // 花心坐标
        _PetalNum("PetalNum", Range(3, 30)) = 15 // 花瓣个数
        _PetalWidth("PetalWidth", Range(0.1, 1)) = 0.5 // 花瓣宽度
        _PetalLength("PetalLength", Range(1, 20)) = 2 // 花瓣长度
        _Speed("Speed", Range(0.2, 5)) = 1 // 花瓣宽度、长度的变化速度
    }

    SubShader{
        Pass {
            CGPROGRAM

            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag

            fixed4 _BackgroundColor; // 背景颜色
            fixed4 _PetalColor; // 花瓣颜色
            fixed4 _StamenColor; // 花蕊颜色
            fixed4 _HeartColor; // 花心颜色
            float4 _Center; // 花蕊中心
            int _PetalNum; // 花瓣个数
            float _PetalWidth; // 花瓣宽度
            float _PetalLength; // 花瓣长度
            float _Speed; // 花瓣宽度、长度的变化速度

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 worldPos : TEXCOORD0; // 纹理uv坐标
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 世界空间顶点坐标
                return o;
            }

            void updateParams() { // 更新花瓣宽度、长度信息
                float time = _Time.y * _Speed;
                _PetalWidth = fmod(time * 0.2, 0.9) + 0.1;
                _PetalLength = fmod(time * 1.1111, 5) + 1;
            }

            float getNearAngle(float3 vec) { // 获取与vec向量方向最近的花瓣的角度
                float proj = dot(normalize(vec), float3(0, 0, 1));
                float angle = acos(proj);
                if (vec.x < 0) { // OP的旋转角度大于180°
                    angle = UNITY_TWO_PI - angle;
                }
                float delta = UNITY_TWO_PI / _PetalNum;
                return round(angle / delta) * delta;
            }

            float3 getStamen(float angle) { // 获取离顶点最近的花蕊位置
                float3 vec = float3(sin(angle), 0, cos(angle));
                return _Center.xyz + vec * _PetalLength;
            }

            float2 getProjXY(float3 vec1, float3 vec2) { // 获取vec1在vec2下的的投影坐标
                float x = abs(dot(vec1, normalize(vec2)));
                float y = sqrt(dot(vec1, vec1) - x * x);
                return float2(x, y);
            }

            fixed4 mixPetalColor(float2 pos) { // 混合花瓣颜色
                float rate = smoothstep(_PetalLength, 0, pos.x) * smoothstep(_PetalWidth, 0, pos.y); // 顶点属于花瓣的比例
                //float rate = smoothstep(sqrt(_PetalLength * _PetalLength + _PetalWidth * _PetalWidth), 0, length(pos));
                return lerp(_BackgroundColor, _PetalColor, rate);
            }

            fixed4 mixStamenColor(fixed4 color, float2 pos) { // 混合花蕊颜色
                float stamenWidth = _PetalWidth * 0.25; // 花蕊宽度
                float len = length(pos); // 顶点离花蕊的长度
                float rate = smoothstep(stamenWidth, 0, len); // 顶点属于花蕊的比例
                return lerp(color, _StamenColor, rate);
            }

            fixed4 mixHeartColor(fixed4 color, float3 pos) { // 混合花心颜色
                float heartWidth = _PetalLength * 0.4; // 花心宽度
                float len = length(pos); // 顶点离花心的长度
                float rate = smoothstep(heartWidth, 0, len); // 顶点属于花蕊的比例
                return lerp(color, _HeartColor, rate);
            }

            fixed4 frag(v2f i) : SV_Target {
                updateParams(); // 更新花瓣宽度、长度信息
                float3 vertVec = i.worldPos - _Center.xyz;
                float nearAngle = getNearAngle(vertVec); // 获取与顶点最近的花瓣角度
                float3 stamen = getStamen(nearAngle); // 获取离顶点最近的花蕊位置
                float2 pos = getProjXY(i.worldPos - stamen, stamen - _Center.xyz); // 顶点在花蕊坐标系下的投影坐标
                fixed4 color = mixPetalColor(pos); // 混合花瓣颜色
                color = mixStamenColor(color, pos); // 混合花蕊颜色
                color = mixHeartColor(color, vertVec); // 混合花心颜色
                return color;
            }

            ENDCG
        }
    }
}

​ 运行效果如下:

3 发射花瓣特效

​ 本节将实现花瓣周期性向四周发射特效,并且在发射过程中向一直旋转。

3.1 花瓣发射原理

1)旋转原理

​ 在第 1 节的基础上,我们需要修改 OS 的旋转角度计算,第 1 个花瓣的旋转角度不再是 0,而是在 0 ~ (2π / _PetalNum) 之间周期性变化,具体计算如下。

cpp 复制代码
float3 vertVec = worldPos - _Center; // 向量OP
float proj = dot(normalize(vertVec), float3(0, 0, 1)); // OP在OZ上的投影
float angle = acos(proj); // 向量OP的角度
if (vertVec.x < 0) { // OP的旋转角度大于180°
	angle = UNITY_TWO_PI - angle;
}
float delta = UNITY_TWO_PI / _PetalNum; // 每个花瓣的角度
float initAngle = fmod(_Time.y * _RotateSpeed, delta); // 第1个花瓣的旋转角度
float k = round((angle - initAngle) / delta); // 旋转的花瓣的个数
float nearAngle = k * delta + initAngle; // 向量OS的角度

2)发射原理

​ 在第 1 节的基础上,我们需要修改 S 点的计算,OS 的长度不再是 _PetalLength,而是随时间逐渐增大的,具体计算如下。

cpp 复制代码
float time = fmod(_Time.y, _CastTime); // 当前发射周期中, 花瓣移动的时间
float dist = _MoveSpeed * time; // 当前发射周期中, 花瓣移动的距离
float len = length(worldPos - _Center); // 当前顶点距离花心的距离
if (len >= dist) { // 顶点在最外一环的花蕊外面
	return dist;
}
float petalLength = _PetalLength * 2; // 花瓣长度
float k = round((dist - len) / petalLength); // 顶点与最外一环的花蕊相隔的花瓣环数
float norm = dist - k * petalLength; // OS模长(与顶点最近一环的花蕊到花心的距离)

​ S 点坐标计算如下。

cpp 复制代码
float3 vec = float3(sin(nearAngle), 0, cos(nearAngle)); // 向量OS的单位方向向量
float3 stamen = _Center + vec * norm; // S点坐标(离P点最近的花蕊坐标)

3.3 花瓣发射实现

​ FlowerEffect.shader

cpp 复制代码
Shader "MyShader/FlowerEffect"  { // 发射花瓣特效
    Properties{
        _BackgroundColor("BackgroundColor", Color) = (1, 1, 1, 1) // 地面颜色
        _PetalColor("PetalColor", Color) = (1, 0, 0, 1) // 花瓣颜色
        _StamenColor("StamenColor", Color) = (0, 1, 0, 1) // 花蕊颜色
        _HeartColor("_HeartColor", Color) = (1, 1, 0, 1) // 花心颜色
        _Center("Center", Vector) = (0, 0, 0, 0) // 花心坐标
        _PetalNum("PetalNum", Range(3, 30)) = 15 // 花瓣个数
        _PetalWidth("PetalWidth", Range(0.1, 1)) = 0.5 // 花瓣宽度
        _PetalLength("PetalLength", Range(1, 20)) = 2 // 花瓣长度
        _RotateSpeed("RotateSpeed", Range(0.2, 5)) = 1 // 花瓣旋转速度
        _MoveSpeed("MoveSpeed", Range(0.2, 5)) = 1 // 花瓣移动速度
        _CastTime("_CastTime", Range(1, 10)) = 5 // 花瓣发射周期
    }

    SubShader{
        Pass {
            CGPROGRAM

            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag

            fixed4 _BackgroundColor; // 背景颜色
            fixed4 _PetalColor; // 花瓣颜色
            fixed4 _StamenColor; // 花蕊颜色
            fixed4 _HeartColor; // 花心颜色
            float4 _Center; // 花蕊中心
            int _PetalNum; // 花瓣个数
            float _PetalWidth; // 花瓣宽度
            float _PetalLength; // 花瓣长度
            float _RotateSpeed; // 花瓣旋转速度
            float _MoveSpeed; // 花瓣移动速度
            float _CastTime; // 花瓣发射周期

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 worldPos : TEXCOORD0; // 纹理uv坐标
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 世界空间顶点坐标
                return o;
            }

            float getLen(float3 vertVec) { // 获取顶点最近的花蕊到花心的距离
                float time = fmod(_Time.y, _CastTime); // 当前发射周期中, 花瓣移动的时间
                float dist = _MoveSpeed * time; // 当前发射周期中, 花瓣移动的距离
                float len = length(vertVec); // 当前顶点距离花心的距离
                if (len >= dist) {
                    return dist;
                }
                float petalLength = _PetalLength * 2;
                return dist - round((dist - len) / petalLength) * petalLength;
            }

            float getNearAngle(float3 vec) { // 获取与vec向量方向最近的花瓣的角度
                float proj = dot(normalize(vec), float3(0, 0, 1));
                float angle = acos(proj);
                if (vec.x < 0) { // OP的旋转角度大于180°
                    angle = UNITY_TWO_PI - angle;
                }
                float delta = UNITY_TWO_PI / _PetalNum;
                float initAngle = fmod(_Time.y * _RotateSpeed, delta);
                float k = round((angle - initAngle) / delta);
                return k * delta + initAngle;
            }

            float3 getStamen(float angle, float len) { // 获取离顶点最近的花蕊位置
                float3 vec = float3(sin(angle), 0, cos(angle));
                return _Center.xyz + vec * len;
            }

            float2 getProjXY(float3 vec1, float3 vec2) { // 获取vec1在vec2下的的投影坐标
                float x = abs(dot(vec1, normalize(vec2)));
                float y = sqrt(dot(vec1, vec1) - x * x);
                return float2(x, y);
            }

            fixed4 mixPetalColor(float2 pos) { // 混合花瓣颜色
                float rate = smoothstep(_PetalLength, 0, pos.x) * smoothstep(_PetalWidth, 0, pos.y); // 顶点属于花瓣的比例
                //float rate = smoothstep(sqrt(_PetalLength * _PetalLength + _PetalWidth * _PetalWidth), 0, length(pos));
                return lerp(_BackgroundColor, _PetalColor, rate);
            }

            fixed4 mixStamenColor(fixed4 color, float2 pos) { // 混合花蕊颜色
                float stamenWidth = _PetalWidth * 0.25; // 花蕊宽度
                float len = length(pos); // 顶点离花蕊的长度
                float rate = smoothstep(stamenWidth, 0, len); // 顶点属于花蕊的比例
                return lerp(color, _StamenColor, rate);
            }

            fixed4 mixHeartColor(fixed4 color, float3 pos) { // 混合花心颜色
                float heartWidth = _PetalLength * 0.4; // 花心宽度
                float len = length(pos); // 顶点离花心的长度
                float rate = smoothstep(heartWidth, 0, len); // 顶点属于花蕊的比例
                return lerp(color, _HeartColor, rate);
            }

            fixed4 frag(v2f i) : SV_Target {
                float3 vertVec = i.worldPos - _Center.xyz;
                float len = getLen(vertVec); // 获取顶点距离最近的内环花蕊的距离
                float nearAngle = getNearAngle(vertVec); // 获取与顶点最近的花瓣角度
                float3 stamen = getStamen(nearAngle, len); // 获取离顶点最近的花蕊位置
                float2 pos = getProjXY(i.worldPos - stamen, stamen - _Center.xyz); // 顶点在花蕊坐标系下的投影坐标
                fixed4 color = mixPetalColor(pos); // 混合花瓣颜色
                color = mixStamenColor(color, pos); // 混合花蕊颜色
                color = mixHeartColor(color, vertVec); // 混合花心颜色
                return color;
            }

            ENDCG
        }
    }
}

​ 运行效果:

​ 声明:本文转自【Unity3D】花瓣特效

相关推荐
Mapmost2 天前
【数据融合实战手册·进阶篇】模型融合总出错?先看看这些“对齐”了没!
unity3d
北桥苏3 天前
如何在 Unity3D 导入 Spine 动画
unity3d
Thomas游戏开发4 天前
Unity3D状态管理器实现指南
前端框架·unity3d·游戏开发
土豆宝10 天前
Unity Visual Scripting(可视化脚本) 自定义节点 踩坑教程
unity3d
Thomas游戏开发10 天前
Unity3D光照层级与动态切换指南
前端框架·unity3d·游戏开发
Thomas游戏开发21 天前
Unity3D 崩溃分析工具的集成与优化
前端框架·unity3d·游戏开发
Thomas游戏开发1 个月前
Unity3D网格简化与LOD技术详解
前端框架·unity3d·游戏开发
Thomas_YXQ1 个月前
Unity3D 图形渲染(Graphics & Rendering)详解
开发语言·unity·图形渲染·unity3d·shader
Thomas游戏开发1 个月前
Unity3D 图形渲染(Graphics & Rendering)详解
前端·unity3d·游戏开发
Thomas游戏开发1 个月前
Unity3D 光栅化 vs 光线追踪:技术详解
前端框架·unity3d·游戏开发