最终效果如下所示,其中包含18个立方体、8个圆形线框的球形,在围绕中心点随时间不断旋转并更改颜色:

如何实现上述效果呢?
首先,在unity中如果要在屏幕上绘制这样的几何体线框并不像在C++等其他语言中一样,需要自己配置渲染管线、操作buffer等,因为这些东西是unity内部已经做好的东西。我们既然不能直接在屏幕上绘制这样的效果,那么怎样才能在unity上实现呢。这时候就想到,屏幕是一个平面,我们在unity中的摄像机屏幕也是屏幕,我们直接把画面绘制到摄像机屏幕上就好了。
所以,可以把shader材质赋给一个平面,再将这个平面正对摄像机就好,这样shader中产生的图像会通过平面来呈现在屏幕上。
部分代码功能解析:
**顶点着色器:**实现将平面上的顶点坐标映射到裁剪空间并输出,再将点映射到屏幕空间输出。
cpp
struct appdata
{
float4 vertex : POSITION; // 顶点输入
};
struct v2f
{
float4 pos : SV_POSITION; // 顶点裁剪空间的输出
float4 screenPos : TEXCOORD0; // 顶点屏幕空间的位置
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 计算顶点在裁剪空间中的位置
o.screenPos = ComputeScreenPos(o.pos); // 计算顶点在屏幕上的位置(没有完成透视除法)
return o;
}
片元着色器:负责渲染屏幕上每一个像素的颜色,这里负责绘制所有线框图形,包含18个线框立方体和8个线框球体。
cpp
fixed4 frag(v2f i) : SV_Target
{
float2 iResolution = _ScreenParams.xy; // 获取屏幕的分辨率
float time = _Time.y * 0.31415; // 将时间乘一个系数,适当缩减变化速率
// 将屏幕像素左边转化为标准的、考虑宽高比的UV坐标
float2 fragCoord = (i.screenPos.xy / max(i.screenPos.w, 1e-6)) * iResolution; // 执行透视除法,将裁剪空间坐标转换到NDC
float2 uv = fragCoord / iResolution; // 归一化到[0,1]
uv = uv * 2.0 - 1.0; // 映射到[-1,1]
uv.x *= iResolution.x / iResolution.y; // 修正宽高比
// 设置一个基础底色,lerp是一个线性差值函数
float3 c = lerp(float3(0.19, 0.13, 0.1), float3(1.0, 1.0, 1.0), 0.5 * pow(length(uv) * 0.5, 2.0));
// 获取旋转角度,与时间有关
float3 rotAngles;
if (_LinearRotation > 0.5)
{
rotAngles = float3(time, time * 0.86, time * 0.473);
}
else
{
float p = 0.08;
rotAngles = float3(
time + sin(time * 30.0) * p,
time * 0.86 + sin(time * 20.0) * p * 1.24,
time * 0.473 + sin(time * 10.0) * p
);
}
// 18个立方体的实例位置
float3 instancesA[18] = {
float3( 0.0, 0.0,-1.0),
float3(-1.0, 0.0,-1.0),
float3( 1.0, 0.0,-1.0),
float3( 0.0, 1.0,-1.0),
float3( 0.0,-1.0,-1.0),
float3(-1.0, 0.0, 0.0),
float3( 1.0, 0.0, 0.0),
float3( 0.0, 1.0, 0.0),
float3( 0.0,-1.0, 0.0),
float3(-1.0,-1.0, 0.0),
float3( 1.0, 1.0, 0.0),
float3(-1.0, 1.0, 0.0),
float3( 1.0,-1.0, 0.0),
float3( 0.0, 0.0, 1.0),
float3(-1.0, 0.0, 1.0),
float3( 1.0, 0.0, 1.0),
float3( 0.0, 1.0, 1.0),
float3( 0.0,-1.0, 1.0)
};
// unroll告诉GPU将下面的循环拆开
[unroll]
// 循环绘制18个立方体
for (int dip = 0; dip < 18; dip++)
{
// 每个立方体实例的8个顶点的位置
float3 v0 = transformPoint(float3(-1.0,-1.0, 1.0), instancesA[dip], rotAngles);
float3 v1 = transformPoint(float3(-1.0, 1.0, 1.0), instancesA[dip], rotAngles);
float3 v2 = transformPoint(float3( 1.0, 1.0, 1.0), instancesA[dip], rotAngles);
float3 v3 = transformPoint(float3( 1.0,-1.0, 1.0), instancesA[dip], rotAngles);
float3 v4 = transformPoint(float3(-1.0,-1.0,-1.0), instancesA[dip], rotAngles);
float3 v5 = transformPoint(float3(-1.0, 1.0,-1.0), instancesA[dip], rotAngles);
float3 v6 = transformPoint(float3( 1.0, 1.0,-1.0), instancesA[dip], rotAngles);
float3 v7 = transformPoint(float3( 1.0,-1.0,-1.0), instancesA[dip], rotAngles);
// 每个立方体顶点的透视后的坐标
v0.z = 1.0 / max(v0.z, 1e-5); v0.xy *= v0.z;
v1.z = 1.0 / max(v1.z, 1e-5); v1.xy *= v1.z;
v2.z = 1.0 / max(v2.z, 1e-5); v2.xy *= v2.z;
v3.z = 1.0 / max(v3.z, 1e-5); v3.xy *= v3.z;
v4.z = 1.0 / max(v4.z, 1e-5); v4.xy *= v4.z;
v5.z = 1.0 / max(v5.z, 1e-5); v5.xy *= v5.z;
v6.z = 1.0 / max(v6.z, 1e-5); v6.xy *= v6.z;
v7.z = 1.0 / max(v7.z, 1e-5); v7.xy *= v7.z;
// 依次计算出当前像素距离每个立方体的12条线的距离,并转换成系数
float edge = 0.0;
edge += lineRaster(uv, v0.xy, v1.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v1.xy, v2.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v2.xy, v3.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v3.xy, v0.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v4.xy, v5.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v5.xy, v6.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v6.xy, v7.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v7.xy, v4.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v0.xy, v4.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v1.xy, v5.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v2.xy, v6.xy, _LineWidth, iResolution);
edge += lineRaster(uv, v3.xy, v7.xy, _LineWidth, iResolution);
// 将影响当前像素颜色的系数转换为颜色
c += fragmentColorFromPos(v0, time) * min(edge, 1.0);
}
// 8个球形实例位置
float3 instancesB[8] = {
float3(-1.0, 1.0,-1.0),
float3( 1.0, 1.0,-1.0),
float3(-1.0,-1.0,-1.0),
float3( 1.0,-1.0,-1.0),
float3(-1.0, 1.0, 1.0),
float3( 1.0, 1.0, 1.0),
float3(-1.0,-1.0, 1.0),
float3( 1.0,-1.0, 1.0)
};
[unroll]
for (int dip2 = 0; dip2 < 8; dip2++)
{
float3 v = transformPoint(float3(0.0, 0.0, 0.0), instancesB[dip2], rotAngles); // 旋转后的球心坐标
v.z = 1.0 / max(v.z, 1e-5);
v.xy *= v.z; // 透视除法
c += fragmentColorFromPos(v, time) * circleRaster(uv, v.xy, v.z, _LineWidth, iResolution);
}
return float4(c, 1.0);
}
**线条抗锯齿函数:**同时负责计算该线条对当前像素的影响因子
cpp
// 线条抗锯齿函数,基于点到线段的距离计算权重
// p: 当前像素位置,p0和p1: 线段的两个端点,w: 线宽参数,iResolution: 屏幕分辨率
float lineRaster(float2 p, float2 p0, float2 p1, float w, float2 iResolution)
{
float2 d = p1 - p0; // 线段方向
float denom = max(dot(d, d), 1e-6); // 线段长度的平方
float t = saturate(dot(d, p - p0) / denom); // 投影参数,限制在[0,1]范围内
float2 proj = p0 + d * t; // 线段上最近点的坐标
float distVal = length(p - proj); // 点到线段的距离
float weight = 3.0 / iResolution.x; // 保证在不同分辨率下,线条的视觉粗细一致
distVal = (1.0 / max(distVal, 1e-5)) * weight * w; // 距离越小,权重越大,乘以线宽参数
return min(distVal * distVal, 1.0); // 权重平方以获得更平滑的抗锯齿效果,最大值为1.0
}

球形抗锯齿函数:基于点到圆心的距离计算权重
cpp
// p: 当前像素位置,c: 圆心位置,r: 圆半径,w: 线宽参数,iResolution: 屏幕分辨率
float circleRaster(float2 p, float2 c, float r, float w, float2 iResolution)
{
float distVal = abs(length(p - c) - r);
float weight = 3.0 / iResolution.x;
distVal = (1.0 / max(distVal, 1e-5)) * weight * w;
return min(distVal * distVal, 1.0);
}
旋转函数:3维空间中的旋转函数
cpp
float3 rotateXYZ(float3 v, float3 a)
{
float sx = sin(a.x), cx = cos(a.x);
float sy = sin(a.y), cy = cos(a.y);
float sz = sin(a.z), cz = cos(a.z);
float3x3 rx = float3x3(
1, 0, 0,
0, cx, -sx,
0, sx, cx
);
float3x3 ry = float3x3(
cy, 0, sy,
0, 1, 0,
-sy, 0, cy
);
float3x3 rz = float3x3(
cz, -sz, 0,
sz, cz, 0,
0, 0, 1
);
return mul(rz, mul(ry, mul(rx, v)));
}

颜色混合函数:根据t参数在a、b、c三种颜色之间进行平滑过渡
cpp
float3 mix3(float3 a, float3 b, float3 c, float t)
{
return (t > 0.5) ? lerp(b, c, t * 2.0 - 1.0) : lerp(a, b, t * 2.0);
}
片段颜色函数:从顶点位置和时间获取片段颜色
cpp
float3 fragmentColorFromPos(float3 p, float time)
{
float t = sin(p.x * 0.8 + time * 0.5) * 0.5 + 0.5;
float fog = min(pow(p.z, 3.0) * 400.0, 1.0);
return mix3(RED, GREEN, BLUE, t) * fog;
}
顶点旋转函数:根据旋转角度和实例位置进行旋转,顺序为缩放、旋转、平移
cpp
float3 transformPoint(float3 localPos, float3 instancePos, float3 rotAngles)
{
float3 p = localPos + instancePos * 4.0;
p = rotateXYZ(p, rotAngles);
p += float3(0.0, 0.0, 10.0);
return p;
}
完整代码链接:https://pan.baidu.com/s/1A2xs5PbxWrtnbPtPQcHfZA?pwd=9527 提取码: 9527