前言

云的实现一般有三种思路:Volume体积云、billboard公告牌、Particle粒子,对于风格化来说体渲染的实现过于真实(但光遇的风格化云貌似是使用体渲染实现的,后面有空会考虑尝试还原还原),而Particle消耗太大了!因此本文将着重展现如何使用公告牌技术实现一个不错的风格化云效果

Youtube上有位大佬讲解了如何使用houdini和ue快速实现一个吉卜力风的卡通云,虽然效果还是ok,但个人认为,这种方法容易露馅。不过,这其中的制作云的思路可以参考,无需再用PS来频繁上色。本篇将在此基础上,添加实现其他的功能(主要思路来自Tyler Smith

Render Type

  • 这里采用半透明效果

  • 实现

    Tags 
    { 
      "RenderPipeline" = "UniversalRenderPipeline"
      "RenderType"="Transparent"
      "Queue" = "Transparent"
      "IgnoreProjector" = "True"
    }
    
    Tags
    {
    "LightMode" = "UniversalForward"
    }
    Blend [_BlendSrc] [_BlendDst]
    BlendOp [_BlendOp]
    ZWrite [_ZWriteMode]
    

不透明度

  • 因为云是一种半透明物体,不能简单地使用diffuse Texture的A通道(为了方便调节美术效果,需要控制它的opacity),所以我用到了DepthFade(弱化半透明物体和不透明物体间相交时产生的硬线)

  • 实现

    • DepthFade
    // UE中的实现
    float DepthFade(in half opacity = 1, in float sceneDepth = 1, in float pixelDepth = 1, in half depthFade = 100)
    {
        half depthDiff = sceneDepth - pixelDepth;
    
        return opacity * saturate(depthDiff / depthFade);
    }
    
    • Scene Depth 和 Pixel Depth
    struct Depth
    {
        float raw;
        float linear01;
        float eye;
    };
    
    // 获取深度图,并提取深度
    Depth SampleDepth(float4 positionSS)
    {
        float4 positionSSNor = float4(positionSS.xyz / positionSS.w, positionSS.w);
    
        Depth depth = (Depth)0;
    
        depth.raw = SampleSceneDepth(positionSSNor.xy);
        depth.eye = LinearEyeDepth(depth.raw, _ZBufferParams);
        depth.linear01 = Linear01Depth(depth.raw, _ZBufferParams);
    
        return depth;
    }
    
    float GetRawDepth(Depth depth)
    {
        return depth.raw;
    }
    
    float GetLinear01Depth(Depth depth)
    {
        return depth.linear01;
    }
    
    float GetEyeDepth(Depth depth)
    {
        return depth.eye;
    }
    
    // positionSS为未进行透视除法的屏幕空间
    float GetPixelDepth(float4 positionSS)
    {
        return positionSS.w;
    }
    
    // 计算得出片元的opacity
    // 这里的albedo是基于POM采样MainTex得到的
    half GetSurfaceOpacity(float4 alebdo, float2 uv, float4 positionSS)
    {
        half surfaceOpacity = SAMPLE_TEXTURE2D(_AlbedoTex, sampler_AlbedoTex, uv).a;
    
        // 计算scene depth 和 pixel depth
        Depth depth = SampleDepth(positionSS);
        float sceneEyeDepth = GetEyeDepth(depth);
        float pixelDepth = GetPixelDepth(positionSS);
    
        // 计算opacity
        float opacity = 1.f;
        _OpacityContrast = max(0.0001f, _OpacityContrast);
        opacity = pow(surfaceOpacity, _OpacityContrast);
        opacity *= pow(alebdo.a, _OpacityContrast);
    
        return DepthFade(sceneEyeDepth, pixelDepth, opacity, _FadeDistance);
    }
    
    // 后续在片元shader对opacity进行clip即可
    
  • 效果
    现在可以通过调节Diffuse Opacity Contrast 和 Depth Fade Distance来控制云的不透明度

自发光

  • 为了控制云的颜色,需要对云上色,分别是红绿蓝三种颜色,这三种颜色控制云的三个区域,随后采样在shader中提取对应通道进行lerp

  • 纹理

    • R通道:基色

    • G通道:水平lerp

    • B通道:边缘

  • 实现

    half3 GetSurfaceEmission(float2 uv)
    {
      half3 o = half3(0, 0, 0);
    
      half3 emissiveColor = SAMPLE_TEXTURE2D(_EmissionTex, sampler_EmissionTex, uv);
      _BaseContrast = max(0.0001f, _BaseContrast);
      _HorizionContrast = max(0.0001f, _HorizionContrast);
      _RimContrast = max(0.0001f, _RimContrast);
      _RimPower = max(0.0001f, _RimPower);
    
      half RChannel = emissiveColor.r;
      RChannel = pow(RChannel, _BaseContrast);
      half GChannel = emissiveColor.g;
      GChannel = pow(GChannel, _HorizionContrast);
      half BChannel = emissiveColor.b;
      BChannel = pow(BChannel, _RimContrast);
    
      o = lerp(_OverlayTint1, _OverlayTint2, RChannel);
      o = lerp(o, _HorizionTint, GChannel);
      o = lerp(o, _RimTint * _RimPower, BChannel);
    
      return o;
    }
    
  • 效果

Parallax Occlusion Mapping

  • 目前实现的云已经有一定的体积感,但为了更好的视觉效果,这里还添加了Parallax Occlusion Mapping视差贴图

  • 实现

    float2 ParallaxOcclusionMapping(Texture2D heightTex, sampler sampler_heightTex, float2 uv, float4 positionCS, half3 viewDirTSNor, half heightRatio, half minLayer, half maxLayer)
    {
      float numLayers = lerp(maxLayer, minLayer, abs(dot(half3(0.h, 0.h, 1.h), viewDirTSNor)));
      float layerHeight = 1.f / numLayers;  // 每层高度
      float currentLayerHeight = 0.f;
    
      // shift of texture coordinates for each layer
      float2 uvDelta = heightRatio * viewDirTSNor.xy / viewDirTSNor.z / numLayers;
      float2 currentUV = uv;
    
      float currentHeightTexValue = GetHeight(currentUV, heightTex, sampler_heightTex);
      while(currentLayerHeight < currentHeightTexValue)
      {
          currentUV -= uvDelta;   // shift of texture coordinates
          currentLayerHeight += layerHeight;  // to next layer
          currentHeightTexValue = GetHeight(currentUV, heightTex, sampler_heightTex); // new height
      }
    
      // last uv
      float2 lastUV = currentUV + uvDelta;
    
      // heights for lerp
      float nextHeight    = currentHeightTexValue - currentLayerHeight;
      float lastHeight    = GetHeight(lastUV, heightTex, sampler_heightTex) - currentLayerHeight + layerHeight;
    
      // proportions for lerp
      float weight = nextHeight / (nextHeight - lastHeight);
    
      // lerp uv
      float2 result = lastUV * weight + currentUV * (1.f-weight);
    
      // lerp depth values
      float parallaxHeight = currentLayerHeight + lastHeight * weight + nextHeight * (1.0 - weight);
    
      return result;
    }
    

动画

  • 最后,为云加上动画,这里使用flowmap,和一个Noise 对floawmap进行扰动

  • 纹理

    • Flowmap

    • Noise

  • 实现

    void CalcFlow(inout float2 uv3, float2 uv4)
    {
      float2 flowValue = SAMPLE_TEXTURE2D(_FlowTex, sampler_FlowTex, uv3).rg;
    
      float2 flowNoisePanner = panner(uv4, _NoisePannerTime * _Time.y, _PannerSpeed);
      float flowNoiseValue = SAMPLE_TEXTURE2D(_FlowNoiseTex, sampler_FlowNoiseTex, flowNoisePanner).r;
      flowNoiseValue *= _FlowPower;
    
      float2 baseUV = lerp(uv3, flowValue, flowNoiseValue);
    
      uv3 = panner(baseUV, _BasePannerTime * _Time.y, _PannerSpeed);
    }
    
  • 效果

reference

吉卜力风的卡通云

Tyler Smith

GLSL 中的视差遮蔽映射


他们曾如此骄傲的活过,贯彻始终