毛发,无论是人类的头发、动物的皮毛,还是奇幻生物的绒毛,都是构成生命感和真实感不可或缺的元素。它对光线的独特散射、吸收和反射,赋予了物体柔软、蓬松、有生命力的质感。它不仅仅是让角色看起来更“毛茸茸”那么简单,更是通向极致真实感和视觉沉浸感的关键一步。本期我们来在Unity6的UDRP项目中实现一个Shell外壳技术的Fur毛发的基础版渲染效果,最终效果如下图所示。

[基础版包含纹理+Shell+AO+边缘光效果]

使用Unity版本:6000.0.43f1

 

        我会先实现UDRP下单Pass(不应用GPUInstancing)+DrawMesh绘制API的方案,后面会再用GPUInstancing(分别使用两种实例绘制API)改进性能。

一.为什么不使用多Pass渲染

1.高DrawCall&打断SRPBatcher

        在URP中,Pass由手动控制,一般在不开启GPU Instancing时,每个Pass都是一次DrawCall,且Shader内多Pass(大部分情况)会导致渲染状态的切换,导致SRP Batcher 无法合批,所以URP中提倡主Pass渲染。

2.RenderFeature实现难度大

        可以使用RenderFeature虽然可以避开Shader内多pass,但是存在注入难度大 & 与多 Pass Shader 协调困难的问题。所以一般是利用RenderFeature插入额外效果,而不是实现Shader 多 Pass。

3.追求优良性能

        多 Pass 意味着多次顶点变换、光照计算、纹理采样 对草,毛发这样一组“重复结构”的对象很不划算,使用URP鼓励的主通道渲染配合GPUInstancing是性能最优的选择。

二.Shell基本原理

        Shell算法可以说是很经典了,网上一搜有大量的介绍,下方的原理图就很直观了,这里我用白话总结一些关键点。

1.顶点沿法线偏移做多壳层

渲染多层壳(Shell,本质是把一个Mesh渲染多次),根据壳层序号作为偏移算子,将顶点沿法线方向偏移一定距离。

2.壳层透明度递减做体积感

采样一张噪声纹理图(仅需读取单通道的黑白信息),根据壳层序号与总壳数的比值 ,将则遮罩图的黑白信息应用到每层壳的透明通道,使外层壳透明度逐渐降低,从而形成多层的体积感。

使用到的毛发噪声及法线贴图:

三.3种Shell_Fur实现方式

        我采用UnlitShader+C#控制脚本的形式实现。在Shader内部,我修改了默认的CGPROGRAM,使用HLSLPROGRAM,对应HLSL语法。

注意引入基本库

 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

1.单通道(非GPUInstancing)+DrawMesh实现基础绘制

(1)Property声明

根据需求我定义了以下属性:

(1)基础Shell:定义了毛发噪声纹理, 毛发(壳层偏移)长度 和 壳层总数;

(1)AO:定义了毛发的根部颜色 和 末端颜色;

(2)边缘光:定义了边缘光颜色 和 菲涅尔强度;

(3)为了便于控制噪声效果,定义剔除阈值;

  Properties{//毛发噪声纹理_FurTex("Fur Texture", 2D) = "white" {}//毛发根部颜色[HDR]_RootColor("RootColor",Color)=(0,0,0,1)//毛发末端颜色[HDR]_FurColor("FurColor",Color)=(1,1,1,1)//凹凸纹理_BumpTex("Normal Map", 2D) = "bump" {}//凹凸强度_BumpIntensity("Bump Intensity",Range(0,2))=1//毛发长度_FurLength("Fur Length", Float) = 0.2//壳层总数_ShellCount("Shell Count", Float) = 16//边缘光颜色[HDR]_FresnelColor("Fresnel Color", Color) = (1,1,1,1)//菲涅尔强度_FresnelPower("Fresnel Power", Float) = 5//噪声剔除阈值_FurAlphaPow("Fur AlphaPow", Range(0,6)) = 1}

(2)顶点沿法线偏移

在顶点着色器中对顶点进行法线方向的偏移

 v2f vert(appdata v)
{v2f o;xxxxxxfloat shellIndex = _ShellIndex;float shellFrac = shellIndex / _ShellCount;float3 worldNormal = TransformObjectToWorldNormal(v.normal);float3 worldPos = TransformObjectToWorld(v.vertex.xyz);worldPos += worldNormal * (_FurLength * shellFrac);xxxxxxreturn o;
}

(3)噪声图透明度递减

 half4 frag(v2f i) : SV_Target{xxxxxxfloat mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r;float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow));xxxxxxxxxcol.a = alpha;xxxreturn col;}

(4)控制脚本传入壳层序号

在 Shader 中 没有直接的方法 获取实例编号(比如第几次调用的实例),比如SV_InstanceID在 Unity 的标准 Shader HLSL 里不暴露。UNITY_GET_INSTANCE_ID

UNITY_GET_INSTANCE_ID() 通常不生效,或者需要使用特殊渲染管线(如 HDRP + DOTS)。

所以,如果你不传 ShellIndex,Shader 就 不知道当前是第几层 Shel

(4)完整代码

C#控制脚本

[RequireComponent((typeof(MeshRenderer)))]
[ExecuteAlways]
public class ShellFurController_NonGpuInstancing : MonoBehaviour
{Mesh mesh;Material material;public int shellCount = 16;private Matrix4x4[] matrices;private MaterialPropertyBlock[] props;void Start(){//不调用 .material,这会创建一个新实例,浪费内存material = GetComponent<MeshRenderer>().sharedMaterial;mesh = GetComponent<MeshFilter>().sharedMesh;matrices = new Matrix4x4[shellCount];props = new MaterialPropertyBlock[shellCount];for (int i = 0; i < shellCount; i++){matrices[i] = transform.localToWorldMatrix;Debug.Log(matrices[i]);props[i] = new MaterialPropertyBlock();props[i].SetFloat("_ShellIndex", i);}}void Update(){//同步更新壳层世界位置for (int i = 0; i < shellCount; i++){matrices[i] = transform.localToWorldMatrix;Debug.Log(matrices[i]);props[i].SetFloat("_ShellIndex", i);}//使用DrawMesh API渲染多壳层for (int i = 0; i < shellCount; i++){Graphics.DrawMesh(mesh,matrices[i],material,           0,null,0,props[i],           UnityEngine.Rendering.ShadowCastingMode.Off,false);}}
}

UnlitShader

Shader "Unlit/Base_Shell_Fur_NonGpuIns"
{Properties{//毛发噪声纹理_FurTex("Fur Texture", 2D) = "white" {}//毛发根部颜色[HDR]_RootColor("RootColor",Color)=(0,0,0,1)//毛发末端颜色[HDR]_FurColor("FurColor",Color)=(1,1,1,1)//凹凸纹理_BumpTex("Normal Map", 2D) = "bump" {}//凹凸强度_BumpIntensity("Bump Intensity",Range(0,2))=1//毛发长度_FurLength("Fur Length", Float) = 0.2//壳层总数_ShellCount("Shell Count", Float) = 16//外发光颜色[HDR]_FresnelColor("Fresnel Color", Color) = (1,1,1,1)//菲涅尔强度_FresnelPower("Fresnel Power", Float) = 5//噪声剔除阈值_FurAlphaPow("Fur AlphaPow", Range(0,6)) = 1}SubShader{Tags { "Queue"="Transparent" "RenderType"="Transparent" }LOD 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{Name "FurPass"Tags { "LightMode" = "UniversalForward" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex);float4 _FurTex_ST;TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex);float _FurLength;float _ShellCount;float4 _FresnelColor;float _FresnelPower;float _FurAlphaPow;float4 _RootColor;float4 _FurColor;float _ShellIndex;//壳层序号,由C#控制脚本传入struct appdata{float4 vertex : POSITION;float3 normal : NORMAL;float2 uv : TEXCOORD0;};struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 viewDir : TEXCOORD1;float3 worldNormal : TEXCOORD2;float shellIndex : TEXCOORD3;};v2f vert(appdata v){float shellIndex = _ShellIndex;float shellFrac = shellIndex / _ShellCount;v2f o;float3 worldNormal = TransformObjectToWorldNormal(v.normal);float3 worldPos = TransformObjectToWorld(v.vertex.xyz);worldPos += worldNormal * (_FurLength * shellFrac);o.pos = TransformWorldToHClip(worldPos);o.uv = TRANSFORM_TEX(v.uv, _FurTex);o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);o.worldNormal = worldNormal;o.shellIndex = shellIndex;return o;}half4 frag(v2f i) : SV_Target{half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv);float shellFrac = i.shellIndex / _ShellCount;float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r;float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow));float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv));float3 normalWS = normalize(i.worldNormal + bump * 0.5);//边缘光float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower);//AOcol*=lerp(_RootColor,_FurColor,shellFrac);col.a = alpha;col.rgb += _FresnelColor.rgb * fresnel * alpha;return col;}ENDHLSL}}
}

(5)效果展示

(6)性能压力!!

当将控制脚本中的ShellCount设置到比较大的数值,我这里发现100层就比较卡了,试想如果这是真实的游戏场景,这性能压力简直是简直了,所以我决定采用GPUInstancing来优化我的项目。

运行前

运行后(场景内漫游时帧率急剧下降)

2.单通道(GPUInstancing)+DrawMeshInstansed实现优良性能

这里我采用Shader内StructruedBuffer搭配C#控制脚本内ComputeBuffer方案实现。

关于GPUInstancing的内容可以移步我的另一篇博客:Unity性能优化-渲染模块(1)-CPU侧(2)-DrawCall优化(2)GPUInstancing-CSDN博客

回顾绘制API的使用可以移步我的另一篇博客:

[学习记录]Unity中的绘制API-CSDN博客

(1)技术要点

1.合并 Draw Call:将所有实例的绘制合并成一个 Draw Call

2.CPU 准备实例数据: 你需要在 CPU 上准备一个所有实例(壳层)的变换矩阵数组,使用ComputeBuffer作为一个所有实例的额外数据数组(例如,包含每个壳层索引的float数组)。通过material.SetBuffer传递给 Shader。

3.Shader 获取实例 ID: Shader 中会启用实例化,并通过内置的SV_InsatnceID获取当前正在处理的实例(壳层)的 ID。

(2)完整代码

C#控制脚本

using UnityEngine;[RequireComponent((typeof(MeshRenderer)))]
[ExecuteAlways]
public class ShellFurController_DrawInstanced : MonoBehaviour
{Mesh mesh; Material material;[Header("壳层数")]public int shellCount = 16;private Matrix4x4[] matrices;//使用DrawInstanced(),为了正确合批,使用统一的MPB,一次绘制所有实例private MaterialPropertyBlock props;private ComputeBuffer shellIndexBuffer;float[] shellIndices;void Start(){material = GetComponent<MeshRenderer>().sharedMaterial;mesh = GetComponent<MeshFilter>().sharedMesh;if (!material.enableInstancing){Debug.LogWarning("Fur material must enable GPU Instancing");}// 所有实例使用同一个 props,用数组传 ShellIndexmatrices = new Matrix4x4[shellCount];props = new MaterialPropertyBlock();shellIndices = new float[shellCount];for (int i = 0; i < shellCount; i++){matrices[i] = transform.localToWorldMatrix;shellIndices[i] = i;}shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float));shellIndexBuffer.SetData(shellIndices);material.SetBuffer("_ShellIndexBuffer", shellIndexBuffer);}void Update(){// 实例位置更新for (int i = 0; i < shellCount; i++){matrices[i] = transform.localToWorldMatrix;}// 使用真正的 GPU Instancing 调用Graphics.DrawMeshInstanced(mesh,0,material,matrices,shellCount,props,UnityEngine.Rendering.ShadowCastingMode.Off,false);}
}

UnlitShader

Shader "Unlit/Base_Shell_Fur_GpuIns"
{Properties{//毛发噪声纹理_FurTex("Fur Texture", 2D) = "white" {}//毛发根部颜色[HDR]_RootColor("RootColor",Color)=(0,0,0,1)//毛发末端颜色[HDR]_FurColor("FurColor",Color)=(1,1,1,1)//凹凸纹理_BumpTex("Normal Map", 2D) = "bump" {}//凹凸强度_BumpIntensity("Bump Intensity",Range(0,2))=1//毛发长度_FurLength("Fur Length", Float) = 0.2//壳层总数_ShellCount("Shell Count", Float) = 16//外发光颜色[HDR]_FresnelColor("Fresnel Color", Color) = (1,1,1,1)//菲涅尔强度_FresnelPower("Fresnel Power", Float) = 5//噪声剔除阈值_FurAlphaPow("Fur AlphaPow", Range(0,6)) = 1}SubShader{Tags { "Queue"="Transparent" "RenderType"="Transparent" }LOD 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{Name "FurPass"Tags { "LightMode" = "UniversalForward" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_instancing#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex);float4 _FurTex_ST;TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex);float4 _BumpTex_ST;float _FurLength;float _ShellCount;float _WindStrength;float4 _FresnelColor;float _FresnelPower;float _FurAlphaPow;float4 _RootColor;float4 _FurColor;StructuredBuffer<float> _ShellIndexBuffer;struct appdata{float4 vertex : POSITION;float3 normal : NORMAL;float2 uv : TEXCOORD0;uint id: SV_InstanceID;};struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 viewDir : TEXCOORD1;float3 worldNormal : TEXCOORD2;float shellIndex : TEXCOORD3;};v2f vert(appdata v){float shellIndex = _ShellIndexBuffer[v.id];float shellFrac = shellIndex / _ShellCount;v2f o;float3 worldNormal = TransformObjectToWorldNormal(v.normal);float3 worldPos = TransformObjectToWorld(v.vertex.xyz);float windOffset = sin(worldPos.x * 5 + _Time.y * 2 + shellIndex) * _WindStrength;worldPos += worldNormal * (_FurLength * shellFrac + windOffset);o.pos = TransformWorldToHClip(worldPos);o.uv = TRANSFORM_TEX(v.uv, _FurTex);o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);o.worldNormal = worldNormal;o.shellIndex = shellIndex;return o;}half4 frag(v2f i) : SV_Target{half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv);float shellFrac = i.shellIndex / _ShellCount;float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r;float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow));float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv));float3 normalWS = normalize(i.worldNormal + bump * 0.5);float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower);//AOcol*=lerp(_RootColor,_FurColor,shellFrac);col.a = alpha;col.rgb += _FresnelColor.rgb * fresnel * alpha;return col;}ENDHLSL}}
}

(3)效果展示

渲染100层前后对比:

savebybatching:0->99

setPass Call : 24->25

这说明我们的GPUInstancng应用成功了。

3.单通道(GPUInstancing)+DrawMeshInstancedIndirect()实现极致性能

(1)技术要点

1.区别于DrawMeshInstanced

DrawMeshInstanced需要在 CPU 端传递一个固定数量的矩阵数组(最大 1023 个实例,超了会加批次),由 CPU 统一调度绘制。

DrawMeshInstancedIndirect是由 GPU 端驱动实例数量,可以动态控制实例数,且不受 1023 个实例的限制,效率更高,尤其适合实例数量变化或复杂实例计算场景。

2.

(2)完整代码

C#控制脚本

using UnityEngine;
using UnityEngine.Rendering;
[ExecuteAlways]
public class ShellFurController_DrawInstancedIndirect : MonoBehaviour
{[Header("壳层数(动态可调)")] public int shellCount = 32;private Mesh mesh;private Material material;private ComputeBuffer argsBuffer;private ComputeBuffer shellIndexBuffer;private int lastShellCount = -1;private Camera mainCam;void Start(){mesh = GetComponent<MeshFilter>().sharedMesh;material = GetComponent<MeshRenderer>().sharedMaterial;mainCam = Camera.main;InitBuffers(); // 初次初始化}void Update(){/*Camera cam = Camera.current;if (!Application.isPlaying && UnityEditor.SceneView.currentDrawingSceneView != null)cam = UnityEditor.SceneView.currentDrawingSceneView.camera;*/if (shellCount != lastShellCount || argsBuffer == null || shellIndexBuffer == null){InitBuffers();lastShellCount = shellCount;}Graphics.DrawMeshInstancedIndirect(mesh,0,material,new Bounds(transform.position, Vector3.one * 100f),argsBuffer,0,null,ShadowCastingMode.On,true,gameObject.layer,mainCam,//这里绑的是Game窗口里的主相机,只会在Game窗口中渲染,场景视图中会不渲染,可以替换成上方的Scene窗口里的camLightProbeUsage.Off);}void InitBuffers(){// 清理旧 bufferargsBuffer?.Release();shellIndexBuffer?.Release();// 初始化 mesh/materialmesh ??= GetComponent<MeshFilter>().sharedMesh; material ??= GetComponent<MeshRenderer>().sharedMaterial;// 创建 DrawMeshInstancedIndirect 参数 bufferuint[] args = new uint[5] {(uint)mesh.GetIndexCount(0),(uint)shellCount,(uint)mesh.GetIndexStart(0),(uint)mesh.GetBaseVertex(0),0};argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);argsBuffer.SetData(args);// 创建 shell index buffer,传给 Shaderfloat[] shellIndices = new float[shellCount];for (int i = 0; i < shellCount; i++)shellIndices[i] = i;shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float));shellIndexBuffer.SetData(shellIndices);// 设置材质参数material.SetBuffer("_ShellIndexBuffer", shellIndexBuffer);material.SetInt("_ShellCount", shellCount);}void OnDisable(){argsBuffer?.Release();shellIndexBuffer?.Release();argsBuffer = null;shellIndexBuffer = null;}#if UNITY_EDITORvoid OnValidate(){lastShellCount = -1; // 强制重建 buffer}
#endif
}

UnlitShader和上面使用DrawMeshInstanced绘制的保持一致

Shader "Unlit/Base_Shell_Fur_GpuIns"
{Properties{//毛发噪声纹理_FurTex("Fur Texture", 2D) = "white" {}//毛发根部颜色[HDR]_RootColor("RootColor",Color)=(0,0,0,1)//毛发末端颜色[HDR]_FurColor("FurColor",Color)=(1,1,1,1)//凹凸纹理_BumpTex("Normal Map", 2D) = "bump" {}//凹凸强度_BumpIntensity("Bump Intensity",Range(0,2))=1//毛发长度_FurLength("Fur Length", Float) = 0.2//壳层总数_ShellCount("Shell Count", Float) = 16//外发光颜色[HDR]_FresnelColor("Fresnel Color", Color) = (1,1,1,1)//菲涅尔强度_FresnelPower("Fresnel Power", Float) = 5//噪声剔除阈值_FurAlphaPow("Fur AlphaPow", Range(0,6)) = 1}SubShader{Tags { "Queue"="Transparent" "RenderType"="Transparent" }LOD 200ZWrite OffCull BackBlend SrcAlpha OneMinusSrcAlphaPass{Name "FurPass"Tags { "LightMode" = "UniversalForward" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_instancing#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"TEXTURE2D(_FurTex); SAMPLER(sampler_FurTex);float4 _FurTex_ST;TEXTURE2D(_BumpTex); SAMPLER(sampler_BumpTex);float4 _BumpTex_ST;float _FurLength;float _ShellCount;float _WindStrength;float4 _FresnelColor;float _FresnelPower;float _FurAlphaPow;float4 _RootColor;float4 _FurColor;StructuredBuffer<float> _ShellIndexBuffer;struct appdata{float4 vertex : POSITION;float3 normal : NORMAL;float2 uv : TEXCOORD0;uint id: SV_InstanceID;};struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 viewDir : TEXCOORD1;float3 worldNormal : TEXCOORD2;float shellIndex : TEXCOORD3;};v2f vert(appdata v){float shellIndex = _ShellIndexBuffer[v.id];float shellFrac = shellIndex / _ShellCount;v2f o;float3 worldNormal = TransformObjectToWorldNormal(v.normal);float3 worldPos = TransformObjectToWorld(v.vertex.xyz);float windOffset = sin(worldPos.x * 5 + _Time.y * 2 + shellIndex) * _WindStrength;worldPos += worldNormal * (_FurLength * shellFrac + windOffset);o.pos = TransformWorldToHClip(worldPos);o.uv = TRANSFORM_TEX(v.uv, _FurTex);o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);o.worldNormal = worldNormal;o.shellIndex = shellIndex;return o;}half4 frag(v2f i) : SV_Target{half4 col = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv);float shellFrac = i.shellIndex / _ShellCount;float mask = SAMPLE_TEXTURE2D(_FurTex, sampler_FurTex, i.uv).r;float alpha= saturate(mask - pow(shellFrac,_FurAlphaPow));float3 bump = UnpackNormal(SAMPLE_TEXTURE2D(_BumpTex, sampler_BumpTex, i.uv));float3 normalWS = normalize(i.worldNormal + bump * 0.5);float fresnel = pow(1.0 - saturate(dot(i.viewDir, normalWS)), _FresnelPower);//AOcol*=lerp(_RootColor,_FurColor,shellFrac);col.a = alpha;col.rgb += _FresnelColor.rgb * fresnel * alpha;return col;}ENDHLSL}}
}

(3)效果展示

(4)与DrawInstanced性能对比

        这里我分别使用Graphics.DrawInstanced()和Graphics.DrawInstancedIndirect() 绘制100000层实例。并通过Stats面板比较运行时的性能。

下图为Graphics.DrawInstancedIndirect() 运行时Stats面板

下图为Graphics.DrawInstanced() 运行时Stats面板

(*)Stats面板上反映性能的指标

(1)CPU: main (ms) / FPS: 直接反映主线程和整体游戏循环的流畅度。主线程耗时越低越好。(2)Batches: 直接反映 Draw Call 数量,越低越好。

(3)Tris / Verts: 直接反映 GPU 需要处理的几何体数量,越低越好。这是这次对比中最关键的性能差异点

(4)render thread (ms):反映了渲染命令提交的效率。

(5)SetPass calls: 反映了 GPU 状态切换开销。

        经过对比我们可以发现,Graphics.DrawInstancedIndirect()之所以能实现“极致性能”,不仅仅因为它减少了 CPU 的 Draw Calls (Batches),更因为它在几何体生成和渲染数量上实现了巨大的优化,将渲染的三角形和顶点数量从千万级别降低到了千级别。

四.总结

        OK,至此我们实现了基本的Shell毛发效果并分别使用两种实例绘制API优化了性能,下一期我们将会完善渲染效果,加入漫反射,kajiya高光和阴影偏移,移动动画和风力扰动效果,最终得到生动的毛发效果,感兴趣的话可以先收藏一手哟!

 

本篇完

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/87540.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87540.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/87540.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字孪生技术引领UI前端设计潮流:增强现实(AR)的集成应用

hello宝子们...我们是艾斯视觉擅长ui设计、前端开发、数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言&#xff1a;AR 与数字孪生融合的设计革新浪潮在体验经济与技术融合的双重驱动下&…

碰一碰发视频源码搭建与定制化开发:支持OEM

在近场通信&#xff08;NFC&#xff09;技术与移动终端深度融合的背景下&#xff0c;“碰一碰发视频” 功能凭借便捷的交互体验&#xff0c;在商业推广、信息传递等场景中快速落地。不同于标准化解决方案的黑盒模式&#xff0c;基于源码的定制化开发能从底层优化传输效率、提升…

Ubuntu 24.04 安装配置 Redis 7.0 开机自启

下载源码 wget https://download.redis.io/releases/redis-7.0.12.tar.gz安装依赖 & 准备环境 sudo apt update && sudo apt upgrade -y sudo apt install -y build-essential tcl curl解压 tar xzf redis-7.0.12.tar.gz编译安装 cd redis-7.0.12 make -j$(nproc) …

【Linux 系统】基础IO——Linux中对文件的理解

13.基础IO(1) 文章目录13.基础IO(1)文件的基本概念&#xff1a;内容与属性文件的打开机制&#xff1a;fopen 和 open被打开的文件与磁盘文件的区别文件的内核数据结构文件与进程的交互方式标准输入/输出/错误与文件流系统调用与文件描述符文件打开模式&#xff08;r/w/a/a&…

Go调度器的抢占机制:从协作式到异步抢占的演进之路|Go语言进阶(7)

想象一下这样的场景&#xff1a;你在餐厅排队等位&#xff0c;前面有个人点了餐却一直霸占着座位玩手机&#xff0c;后面的人只能干等着。这就是Go早期版本面临的问题——一个goroutine如果不主动让出CPU&#xff0c;其他goroutine就只能饿着。 今天我们来聊聊Go调度器是如何解…

开源模型应用落地-让AI更懂你的每一次交互-Mem0集成Qdrant、Neo4j与Streamlit的创新实践(四)

一、前言 在人工智能迅速发展的今天,如何让AI系统更懂“你”?答案或许藏在个性化的记忆管理之中。Mem0作为一个开源的记忆管理系统,正致力于为AI赋予长期记忆与个性化服务能力。通过结合高性能向量数据库Qdrant、图数据库Neo4j的强大关系分析能力以及Streamlit的高效可视化交…

基于微信小程序的校园二手交易平台、微信小程序校园二手商城源代码+数据库+使用说明,layui+微信小程序+Spring Boot

school-market 介绍 基于微信小程序的校园二手交易平台 功能结构图 软件架构 系统分为三个端&#xff0c;分别是客户端、管理端、服务端&#xff1b; 客户端&#xff1a;使用原生微信小程序实现 管理端&#xff1a;使用Layui实现 服务端&#xff1a;使用Java SpringBoot…

IDEA与Gradle构建冲突,导致java重复类的解决方案

项目构建总是报错&#xff1a;错误提示1&#xff1a;java:重复类或错误提示2&#xff1a;Internal error in the mapping processor: java.lang.RuntimeException: javax.annotation.processing.FilerException: Attempt to recreate a file排查发现build/generated/sources/an…

如何调节笔记本电脑亮度?其实有很多种方式可以调整亮度

长时间面对屏幕工作、学习或娱乐&#xff0c;很多人会感到眼睛干涩、疲劳&#xff0c;甚至出现视力下降等问题。其实&#xff0c;这些问题的背后&#xff0c;往往隐藏着一个看似简单却极易被忽视的设置—屏幕亮度。 合适的屏幕亮度不仅能提升视觉体验&#xff0c;还能有效缓解…

国际数字影像产业园创作空间升级 打造更优质营商环境

国际数字影像产业园创作空间升级后表现显著&#xff0c;聚焦设施数字化与用户体验优化。整体提升了创意生态系统的竞争力&#xff0c;有效吸引全球企业。 升级核心改进 基础设施现代化&#xff1a;引入智能硬件如5G网络和云渲染设备&#xff0c;支持高清影像处理&#xff0c;…

浅谈 webshell 构造之如何获取恶意函数

前言这篇文章主要是总结一下自己学习过的如何获取恶意函数的篇章&#xff0c;重点是在如何获取恶意函数get_defined_functions(PHP 4 > 4.0.4, PHP 5, PHP 7, PHP 8)get_defined_functions — 返回所有已定义函数的数组我们主要是可以通过这个获取危险的函数比如比如当然还有…

Python 单例模式与魔法方法:深度解析与实践应用

在 Python 编程领域,设计模式解决常见问题的通用方案,而魔法方法则是 Python 语言赋予类强大功能的特殊接口。单例模式和魔法方法看似独立,实则紧密关联,魔法方法常被用于实现单例模式。深入理解并熟练运用它们,能够帮助开发者编写出结构清晰、高效且具有高复用性的代码。…

pybind11 导出 C++ map 在 Python 层 get 访问慢的优化方案

pybind11 导出 C map 在 Python 层 get 访问慢的优化方案 问题描述 通过 pybind11 导出 C 的 std::map 或 std::unordered_map&#xff0c;在 Python 代码中频繁使用 get 方法访问 value 时&#xff0c;性能非常低下。其主要原因是&#xff1a; pybind11 的 map 绑定会导致每次…

RTC实时时钟DS1339U-33国产替代FRTC1339M

FRTC1339M是一款实时时钟&#xff08;RTC&#xff09;芯片&#xff0c;由NYFEA徕飞公司制造。 FRTC13399M串行实时时钟是一种低功耗的时钟日期设备&#xff0c;具有两个可编程的每日时间警报和一个可编程的方波输出。通过2线双向总线进行串行地址和数据传输。时钟/日期提供秒、…

网络常用端口号归纳

ICMP端口号&#xff1a;1IGMP端口号&#xff1a;2TCP端口号&#xff1a;6UDP端口号&#xff1a;17FTP端口号&#xff1a;20(控制信息传输)、21&#xff08;数据传输&#xff09;SSH端口号&#xff1a;22Telnet端口号&#xff1a;23SMTP端口号&#xff1a;25IPV6端口号&#xff…

Agent learn

1.人物设定&#xff1a; 1.1塑造智能体的思维能力与问题拆解与拆解分析能力 1.2个性化&#xff1a;输出预期输出示例&#xff08;设定智能体的-》性格&#xff0c;语言风格&#xff09; 1.3插件&#xff0c;调用工具 1.4可设定结构化表达 1.5调优 1.6常见问题&#xff1a; …

五层协议介绍

层次核心功能典型协议/设备应用层为用户应用程序提供网络服务接口&#xff08;如文件传输、电子邮件、网页浏览&#xff09;HTTP、FTP、SMTP、DNS、SSH传输层提供端到端的可靠或不可靠数据传输&#xff0c;处理流量控制和差错恢复TCP&#xff08;可靠&#xff09;、UDP&#xf…

gin框架 中间件 是在判断路由存在前执行还是存在后执行的研究

最近有个需求&#xff0c;就是发现我们的验签路由中间件会在判断路由是否存在前执行。我们期望是gin框架先自己判断路由中间件是否存在&#xff0c;存在了再走后面的中间件&#xff0c;不存在直接返回404.这样能节省一定的资源。 研究了一下gin框架的源码&#xff0c; 先说一下…

AGV 无人叉车关键技术问题解析:精准定位算法 / 安全避障逻辑 / 系统对接协议全方案

AGV无人叉车作为智能物流的核心装备&#xff0c;在落地时常面临定位漂移、系统兼容性差、避障失灵等痛点。本文深度解析5大高频问题成因与解决方案&#xff0c;助企业规避运营风险&#xff0c;提升效率。 一、定位导航问题&#xff1a;行驶路径偏移怎么办&#xff1f; 1.典型…

AI Agent意图识别

意图识别&#xff1a;多维度拆解 意图识别是人机对话系统&#xff08;Conversational AI&#xff09;的“大脑皮层”&#xff0c;负责理解用户言语背后的真实目的。它将用户的自然语言输入映射到一个预定义的意图类别上。可以说&#xff0c;意图识别的准确性&#xff0c;直接决…