众所周知,我们平时玩手机游戏接触到的游戏UI的资源图,大多数都是设计师童鞋使用Photoshop绘制出来的。那么,作为程序员,就可能时不时遇到这样的需求:
a、游戏里的一些图片元素,在不同的主题里,呈现不同的色彩,但是又不是简单的调色。美术童鞋为了达到美观的效果,会使用Photoshop里面的图层混合模式,将颜色以某种混合模式作用到一张灰白底图。这样就不需要每次都给程序员重新绘制图片,不同的地方只需要给一个色号,用上一样的混合模式,就能复刻一样的效果了。
b、游戏里的一些UI模拟静态光效等,在Photoshop里,设计师是拿背景图+花纹图样混合而来,这样的话,程序员就需要在代码里实现这样的图层混合效果。
那么,这种时候,虽然咱程序员并不是设计师,也需要了解一下Photoshop里面的图层混合模式了。
首先可以看看都有哪些图层混合模式,以及一些简单的描述:
(以下资料由AI大模型总结,如有错误请轻喷哈):
英文名称 | 中文名称 | 描述 |
---|---|---|
Normal | 正常 | 在“运算”或“应用图像”时使用该模式完全不加混合地将源图层或通道复制到目标图层,通道,也就是用源完全替代目标。 |
Dissolve | 溶解 | 把溶解的不透明度作为来自混合色的像素百分比并按此比例把混合色放于基色之上。 |
Multiply | 正片叠底 | 任何原来每张图像上黑的部分在结果中为黑,任何在原来每张图像上白的或是被清除的部分会让你透过它看到另外一幅图像上相同位置的部分。 |
Screen | 屏幕 | 它是一个和正片叠底相反的操作过程,在所有的模式里也都有。它所展现的效果是:在图像中白色的部分在结果中也是白色,在图像中黑色的部分在结果中显示出另一幅图像相同位置的部分。 |
Soft Light | 柔光 | 在这模式下,原始图像与混合色彩,图案或图像进行混合,并依据混合图像决定使原始图像变亮还是变暗,混合图像亮则更亮,暗则更暗。 |
Hard Light | 强光 | 这没什么好讲的, 在这模式下结果图像是从混合色彩,图案或图像中取的其亮度值。 |
Overlay | 叠加 | 它是正片叠底(Multipy)和屏幕(Screen)模式的合并。效果:原始图像中黑暗的区域被叠底而光亮的区域就被屏幕化,最亮的部分和阴影部分被一定程度的保存下来。 |
Darken | 暗化 | 将两个图像中更暗的那个被选来作为结果。 |
Lighten | 亮化 | 是取两个像素中更亮的作为结果。 上述模式在合并几个蒙版以创建新的蒙版时最有用。 |
Difference | 差值 | 比较两个图像并给出一个蒙版,这个蒙版在两幅图像完全相同的区域为黑色,并随两幅图像相差越大它越趋向于白色。 |
Exclusion | 排除 | 与差值模式差不了多少,很相似,除了有强度上的差别外。(注:一个用黑色所作的排除将不会改变任何原始图像) |
Add | 相加 | 将原始图像及混合图像的对应像素取出来并加在一起。 |
Subtract | 相减 | 将原始图像与混合图像相对应的像素提取出来并将它们相减。 |
Hue | 色相 | 把这些模式放在一起,是因为上述的混合模式可以通过混合色彩,图案或是图像的色相,饱和度,色彩,亮度作为原始图像的色相,饱和度,色彩以及亮度来影响原始图像。 |
Saturation | 饱和度 | 把这些模式放在一起,是因为上述的混合模式可以通过混合色彩,图案或是图像的色相,饱和度,色彩,亮度作为原始图像的色相,饱和度,色彩以及亮度来影响原始图像。 |
Color | 色彩 | 把这些模式放在一起,是因为上述的混合模式可以通过混合色彩,图案或是图像的色相,饱和度,色彩,亮度作为原始图像的色相,饱和度,色彩以及亮度来影响原始图像。 |
Luminosity | 亮度 | 把这些模式放在一起,是因为上述的混合模式可以通过混合色彩,图案或是图像的色相,饱和度,色彩,亮度作为原始图像的色相,饱和度,色彩以及亮度来影响原始图像。 |
我们可以实现自定义的shader来实现上面的这些效果。
但是在实现shader之前,我们需要准确的知道这些混合模式对应的实现原理,实质上就是需要知道当我们有两个颜色值的时候,用什么样的数学公式能将他们正确的混合成对应的效果。
这里有一份Adobe早期官方对Blend Mode的说明性pdf文件,里面详细记录了几个常见的混合模式的原理和计算公式,大家取需:
Adobe Blend Modes.pdf
以正片叠底为例,其英文名字Multiply就是乘法的意思,我们可以看到,正片叠底的计算公式为:
实际shader实现就是一个采样到的颜色相乘的结果。
这里推荐Github上面一个开源项目:
JL-s-Unity-Blend-Modes
这个开源项目给出了常见的图层混合模式的实现代码。
这个仓库主要是以Grab屏幕像素,然后叠加上一张图片去做的屏幕特效为例,但是我们可以很轻松的将其改造为上文我们提到的用一个颜色和灰白图混合得到效果,也可以轻松的改造成两张图片采样去混合。
这个仓库给我们提供了常见混合模式的shader公式实现,比如一些常见的计算工具函数
#ifndef PHOTOSHOP_BLENDMODES_INCLUDED
#define PHOTOSHOP_BLENDMODES_INCLUDED
//
// Ported from https://www.shadertoy.com/view/XdS3RW
//
// Original License:
//
// Creative Commons CC0 1.0 Universal (CC-0)
//
// 25 of the layer blending modes from Photoshop.
//
// The ones I couldn't figure out are from Nvidia's advanced blend equations extension spec -
// http://www.opengl.org/registry/specs/NV/blend_equation_advanced.txt
//
// ~bj.2013
//
// Helpers
const fixed3 l = fixed3(0.3, 0.59, 0.11);
/** @private */
float pinLight(float s, float d)
{
return (2.0*s - 1.0 > d) ? 2.0*s - 1.0 : (s < 0.5 * d) ? 2.0*s : d;
}
/** @private */
float vividLight(float s, float d)
{
return (s < 0.5) ? 1.0 - (1.0 - d) / (2.0 * s) : d / (2.0 * (1.0 - s));
}
/** @private */
float hardLight(float s, float d)
{
return (s < 0.5) ? 2.0*s*d : 1.0 - 2.0*(1.0 - s)*(1.0 - d);
}
/** @private */
float softLight(float s, float d)
{
return (s < 0.5) ? d - (1.0 - 2.0*s)*d*(1.0 - d)
: (d < 0.25) ? d + (2.0*s - 1.0)*d*((16.0*d - 12.0)*d + 3.0)
: d + (2.0*s - 1.0) * (sqrt(d) - d);
}
/** @private */
float overlay( float s, float d )
{
return (d < 0.5) ? 2.0*s*d : 1.0 - 2.0*(1.0 - s)*(1.0 - d);
}
// rgb<-->hsv functions by Sam Hocevar
// http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
/** @private */
fixed3 rgb2hsv(fixed3 c)
{
fixed4 K = fixed4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
fixed4 p = lerp(fixed4(c.bg, K.wz), fixed4(c.gb, K.xy), step(c.b, c.g));
fixed4 q = lerp(fixed4(p.xyw, c.r), fixed4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return fixed3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
/** @private */
fixed3 hsv2rgb(fixed3 c)
{
fixed4 K = fixed4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
fixed3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
// Public API Blend Modes
fixed3 ColorBurn(fixed3 s, fixed3 d)
{
return 1.0 - (1.0 - d) / s;
}
fixed3 LinearBurn(fixed3 s, fixed3 d )
{
return s + d - 1.0;
}
fixed3 DarkerColor(fixed3 s, fixed3 d)
{
return (s.x + s.y + s.z < d.x + d.y + d.z) ? s : d;
}
fixed3 Lighten(fixed3 s, fixed3 d)
{
return max(s, d);
}
fixed3 Screen(fixed3 s, fixed3 d)
{
return s + d - s * d;
}
fixed3 ColorDodge(fixed3 s, fixed3 d)
{
return d / (1.0 - s);
}
fixed3 LinearDodge(fixed3 s, fixed3 d)
{
return s + d;
}
fixed3 LighterColor(fixed3 s, fixed3 d)
{
return (s.x + s.y + s.z > d.x + d.y + d.z) ? s : d;
}
fixed3 Overlay(fixed3 s, fixed3 d)
{
fixed3 c;
c.x = overlay(s.x, d.x);
c.y = overlay(s.y, d.y);
c.z = overlay(s.z, d.z);
return c;
}
fixed3 SoftLight(fixed3 s, fixed3 d)
{
fixed3 c;
c.x = softLight(s.x, d.x);
c.y = softLight(s.y, d.y);
c.z = softLight(s.z, d.z);
return c;
}
fixed3 HardLight(fixed3 s, fixed3 d)
{
fixed3 c;
c.x = hardLight(s.x, d.x);
c.y = hardLight(s.y, d.y);
c.z = hardLight(s.z, d.z);
return c;
}
fixed3 VividLight(fixed3 s, fixed3 d)
{
fixed3 c;
c.x = vividLight(s.x, d.x);
c.y = vividLight(s.y, d.y);
c.z = vividLight(s.z, d.z);
return c;
}
fixed3 LinearLight(fixed3 s, fixed3 d)
{
return 2.0*s + d - 1.0;
}
fixed3 PinLight(fixed3 s, fixed3 d)
{
fixed3 c;
c.x = pinLight(s.x, d.x);
c.y = pinLight(s.y, d.y);
c.z = pinLight(s.z, d.z);
return c;
}
fixed3 HardMix(fixed3 s, fixed3 d)
{
return floor(s+d);
}
fixed3 Difference(fixed3 s, fixed3 d)
{
return abs(d-s);
}
fixed3 Exclusion(fixed3 s, fixed3 d)
{
return s + d - 2.0*s*d;
}
fixed3 Subtract(fixed3 s, fixed3 d)
{
return s-d;
}
fixed3 Divide(fixed3 s, fixed3 d)
{
return s/d;
}
fixed3 Add(fixed3 s, fixed3 d)
{
return s+d;
}
fixed3 Hue(fixed3 s, fixed3 d)
{
d = rgb2hsv(d);
d.x = rgb2hsv(s).x;
return hsv2rgb(d);
}
fixed3 Color(fixed3 s, fixed3 d)
{
s = rgb2hsv(s);
s.z = rgb2hsv(d).z;
return hsv2rgb(s);
}
fixed3 Saturation(fixed3 s, fixed3 d)
{
d = rgb2hsv(d);
d.y = rgb2hsv(s).y;
return hsv2rgb(d);
}
fixed3 Luminosity(fixed3 s, fixed3 d)
{
float dLum = dot(d, l);
float sLum = dot(s, l);
float lum = sLum - dLum;
fixed3 c = d + lum;
float minC = min(min(c.x, c.y), c.z);
float maxC = max(max(c.x, c.y), c.z);
if(minC < 0.0) return sLum + ((c - sLum) * sLum) / (sLum - minC);
else if(maxC > 1.0) return sLum + ((c - sLum) * (1.0 - sLum)) / (maxC - sLum);
else return c;
}
#endif // PHOTOSHOP_BLENDMODES_INCLUDED
以色相(Hue)模式为例:
Shader "Blendmodes/Hue"
{
Properties
{
[Header(Properties)]
_MainTex ("Blend Texture", 2D) = "white" {}
_Tint1 ("Tint on Texture", Color) = (1,1,1,0)
_Tint2 ("Tint on Blended Result", Color) = (1,1,1,0)
_Alpha ("Opacity of Blended Result", Range(0.0, 1.0)) = 1.0
//blending
[Header(Blending)]
[Enum(UnityEngine.Rendering.BlendMode)] _BlendSrc ("Blend mode Source", Int) = 5
[Enum(UnityEngine.Rendering.BlendMode)] _BlendDst ("Blend mode Destination", Int) = 10
// required for UI.Mask
[Header(Stencil)]
[Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
[Enum(UnityEngine.Rendering.StencilOp)] _StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
[Enum(None,0,Alpha,1,Red,8,Green,4,Blue,2,RGB,14,RGBA,15)] _ColorMask("Color Mask", Int) = 15
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="true" "RenderType"="Transparent" }
LOD 100
Blend [_BlendSrc] [_BlendDst]
// required for UI.Mask
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
GrabPass
{
"_BackgroundTexture"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "CGIncludes/PhotoshopBlendModes.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 color : COLOR;
float2 bguv : TEXCOORD1;
};
sampler2D _MainTex;
fixed4 _MainTex_ST;
fixed4 _Tint1;
fixed4 _Tint2;
fixed _Alpha;
sampler2D _BackgroundTexture;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color = v.color;
o.bguv = ComputeGrabScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 mainColor = tex2D(_BackgroundTexture, i.bguv);
fixed4 blendColor = tex2D(_MainTex, i.uv) * i.color;
blendColor.xyz += _Tint1.xyz * _Tint1.a;
// perform blend
mainColor.xyz = Hue(mainColor.xyz, blendColor.xyz);
mainColor.xyz += _Tint2.xyz * _Tint2.a;
mainColor.a = blendColor.a * _Alpha;
return mainColor;
}
ENDCG
}
}
}
这个仓库不管是作为shader学习,还是参考实现公式,都是非常赞的。