1 namespace UnityEngine
.Rendering
.PostProcessing
4 /// A raw implementation of John Hable's artist-friendly tonemapping curve.
5 /// See http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/
7 public class HableCurve
18 public float Eval(float x
)
20 float x0
= (x
- offsetX
) * scaleX
;
23 // log(0) is undefined but our function should evaluate to 0. There are better ways to handle this,
24 // but it's doing it the slow way here for clarity.
26 y0
= Mathf
.Exp(lnA
+ B
* Mathf
.Log(x0
));
28 return y0
* scaleY
+ offsetY
;
40 internal float overshootX
;
41 internal float overshootY
;
47 /// The curve's white point.
49 public float whitePoint { get; private set; }
52 /// The inverse of the curve's white point.
54 public float inverseWhitePoint { get; private set; }
56 internal float x0 { get; private set; }
57 internal float x1 { get; private set; }
60 readonly Segment
[] m_Segments
= new Segment
[3];
63 /// Creates a new curve.
67 for (int i
= 0; i
< 3; i
++)
68 m_Segments
[i
] = new Segment();
70 uniforms
= new Uniforms(this);
74 /// Evaluates a given point on the curve.
76 /// <param name="x">The point within the curve to evaluate (on the horizontal axis)</param>
77 /// <returns>The value of the curve, at the point specified</returns>
78 public float Eval(float x
)
80 float normX
= x
* inverseWhitePoint
;
81 int index
= (normX
< x0
) ? 0 : ((normX
< x1
) ? 1 : 2);
82 var segment
= m_Segments
[index
];
83 float ret
= segment
.Eval(normX
);
88 /// Initializes the curve with given settings.
90 /// <param name="toeStrength">Affects the transition between the toe and the mid section of
91 /// the curve. A value of 0 means no toe, a value of 1 means a very hard transition</param>
92 /// <param name="toeLength">Affects how much of the dynamic range is in the toe. With a
93 /// small value, the toe will be very short and quickly transition into the linear section,
94 /// and with a longer value having a longer toe</param>
95 /// <param name="shoulderStrength">Affects the transition between the mid section and the
96 /// shoulder of the curve. A value of 0 means no shoulder, a value of 1 means a very hard
97 /// transition</param>
98 /// <param name="shoulderLength">Affects how many F-stops (EV) to add to the dynamic range
99 /// of the curve</param>
100 /// <param name="shoulderAngle">Affects how much overshoot to add to the shoulder</param>
101 /// <param name="gamma">Applies a gamma function to the curve</param>
102 public void Init(float toeStrength
, float toeLength
, float shoulderStrength
, float shoulderLength
, float shoulderAngle
, float gamma
)
104 var dstParams
= new DirectParams();
106 // This is not actually the display gamma. It's just a UI space to avoid having to
107 // enter small numbers for the input.
108 const float kPerceptualGamma
= 2.2f
;
112 toeLength
= Mathf
.Pow(Mathf
.Clamp01(toeLength
), kPerceptualGamma
);
113 toeStrength
= Mathf
.Clamp01(toeStrength
);
114 shoulderAngle
= Mathf
.Clamp01(shoulderAngle
);
115 shoulderStrength
= Mathf
.Clamp(shoulderStrength
, 1e-5f
, 1f
- 1e-5f
);
116 shoulderLength
= Mathf
.Max(0f
, shoulderLength
);
117 gamma
= Mathf
.Max(1e-5f
, gamma
);
122 // Toe goes from 0 to 0.5
123 float x0
= toeLength
* 0.5f
;
124 float y0
= (1f
- toeStrength
) * x0
; // Lerp from 0 to x0
126 float remainingY
= 1f
- y0
;
128 float initialW
= x0
+ remainingY
;
130 float y1_offset
= (1f
- shoulderStrength
) * remainingY
;
131 float x1
= x0
+ y1_offset
;
132 float y1
= y0
+ y1_offset
;
134 // Filmic shoulder strength is in F stops
135 float extraW
= RuntimeUtilities
.Exp2(shoulderLength
) - 1f
;
137 float W
= initialW
+ extraW
;
145 // Bake the linear to gamma space conversion
146 dstParams
.gamma
= gamma
;
149 dstParams
.overshootX
= (dstParams
.W
* 2f
) * shoulderAngle
* shoulderLength
;
150 dstParams
.overshootY
= 0.5f
* shoulderAngle
* shoulderLength
;
152 InitSegments(dstParams
);
155 void InitSegments(DirectParams srcParams
)
157 var paramsCopy
= srcParams
;
159 whitePoint
= srcParams
.W
;
160 inverseWhitePoint
= 1f
/ srcParams
.W
;
162 // normalize params to 1.0 range
164 paramsCopy
.x0
/= srcParams
.W
;
165 paramsCopy
.x1
/= srcParams
.W
;
166 paramsCopy
.overshootX
= srcParams
.overshootX
/ srcParams
.W
;
169 float shoulderM
= 0f
;
172 AsSlopeIntercept(out m
, out b
, paramsCopy
.x0
, paramsCopy
.x1
, paramsCopy
.y0
, paramsCopy
.y1
);
174 float g
= srcParams
.gamma
;
176 // Base function of linear section plus gamma is
179 // which we can rewrite as
180 // y = exp(g*ln(m) + g*ln(x+b/m))
182 // and our evaluation function is (skipping the if parts):
184 float x0 = (x - offsetX) * scaleX;
185 y0 = exp(m_lnA + m_B*log(x0));
186 return y0*scaleY + m_offsetY;
189 var midSegment
= m_Segments
[1];
190 midSegment
.offsetX
= -(b
/ m
);
191 midSegment
.offsetY
= 0f
;
192 midSegment
.scaleX
= 1f
;
193 midSegment
.scaleY
= 1f
;
194 midSegment
.lnA
= g
* Mathf
.Log(m
);
197 toeM
= EvalDerivativeLinearGamma(m
, b
, g
, paramsCopy
.x0
);
198 shoulderM
= EvalDerivativeLinearGamma(m
, b
, g
, paramsCopy
.x1
);
200 // apply gamma to endpoints
201 paramsCopy
.y0
= Mathf
.Max(1e-5f
, Mathf
.Pow(paramsCopy
.y0
, paramsCopy
.gamma
));
202 paramsCopy
.y1
= Mathf
.Max(1e-5f
, Mathf
.Pow(paramsCopy
.y1
, paramsCopy
.gamma
));
204 paramsCopy
.overshootY
= Mathf
.Pow(1f
+ paramsCopy
.overshootY
, paramsCopy
.gamma
) - 1f
;
207 this.x0
= paramsCopy
.x0
;
208 this.x1
= paramsCopy
.x1
;
212 var toeSegment
= m_Segments
[0];
213 toeSegment
.offsetX
= 0;
214 toeSegment
.offsetY
= 0f
;
215 toeSegment
.scaleX
= 1f
;
216 toeSegment
.scaleY
= 1f
;
219 SolveAB(out lnA
, out B
, paramsCopy
.x0
, paramsCopy
.y0
, toeM
);
220 toeSegment
.lnA
= lnA
;
226 // Use the simple version that is usually too flat
227 var shoulderSegment
= m_Segments
[2];
229 float x0
= (1f
+ paramsCopy
.overshootX
) - paramsCopy
.x1
;
230 float y0
= (1f
+ paramsCopy
.overshootY
) - paramsCopy
.y1
;
233 SolveAB(out lnA
, out B
, x0
, y0
, shoulderM
);
235 shoulderSegment
.offsetX
= (1f
+ paramsCopy
.overshootX
);
236 shoulderSegment
.offsetY
= (1f
+ paramsCopy
.overshootY
);
238 shoulderSegment
.scaleX
= -1f
;
239 shoulderSegment
.scaleY
= -1f
;
240 shoulderSegment
.lnA
= lnA
;
241 shoulderSegment
.B
= B
;
244 // Normalize so that we hit 1.0 at our white point. We wouldn't have do this if we
245 // skipped the overshoot part.
247 // Evaluate shoulder at the end of the curve
248 float scale
= m_Segments
[2].Eval(1f
);
249 float invScale
= 1f
/ scale
;
251 m_Segments
[0].offsetY
*= invScale
;
252 m_Segments
[0].scaleY
*= invScale
;
254 m_Segments
[1].offsetY
*= invScale
;
255 m_Segments
[1].scaleY
*= invScale
;
257 m_Segments
[2].offsetY
*= invScale
;
258 m_Segments
[2].scaleY
*= invScale
;
262 // Find a function of the form:
263 // f(x) = e^(lnA + Bln(x))
265 // f(0) = 0; not really a constraint
268 void SolveAB(out float lnA
, out float B
, float x0
, float y0
, float m
)
271 lnA
= Mathf
.Log(y0
) - B
* Mathf
.Log(x0
);
275 void AsSlopeIntercept(out float m
, out float b
, float x0
, float x1
, float y0
, float y1
)
277 float dy
= (y1
- y0
);
278 float dx
= (x1
- x0
);
289 // f'(x) = gm(mx+b)^(g-1)
290 float EvalDerivativeLinearGamma(float m
, float b
, float g
, float x
)
292 float ret
= g
* m
* Mathf
.Pow(m
* x
+ b
, g
- 1f
);
297 /// Utility class to retrieve curve values for shader evaluation.
299 public class Uniforms
303 internal Uniforms(HableCurve parent
)
305 this.parent
= parent
;
309 /// A pre-built <see cref="Vector4"/> holding: <c>(inverseWhitePoint, x0, x1, 0)</c>.
313 get { return new Vector4(parent.inverseWhitePoint, parent.x0, parent.x1, 0f); }
317 /// A pre-built <see cref="Vector4"/> holding: <c>(toe.offsetX, toe.offsetY, toe.scaleX, toe.scaleY)</c>.
319 public Vector4 toeSegmentA
323 var toe
= parent
.m_Segments
[0];
324 return new Vector4(toe
.offsetX
, toe
.offsetY
, toe
.scaleX
, toe
.scaleY
);
329 /// A pre-built <see cref="Vector4"/> holding: <c>(toe.lnA, toe.B, 0, 0)</c>.
331 public Vector4 toeSegmentB
335 var toe
= parent
.m_Segments
[0];
336 return new Vector4(toe
.lnA
, toe
.B
, 0f
, 0f
);
341 /// A pre-built <see cref="Vector4"/> holding: <c>(mid.offsetX, mid.offsetY, mid.scaleX, mid.scaleY)</c>.
343 public Vector4 midSegmentA
347 var mid
= parent
.m_Segments
[1];
348 return new Vector4(mid
.offsetX
, mid
.offsetY
, mid
.scaleX
, mid
.scaleY
);
353 /// A pre-built <see cref="Vector4"/> holding: <c>(mid.lnA, mid.B, 0, 0)</c>.
355 public Vector4 midSegmentB
359 var mid
= parent
.m_Segments
[1];
360 return new Vector4(mid
.lnA
, mid
.B
, 0f
, 0f
);
365 /// A pre-built <see cref="Vector4"/> holding: <c>(toe.offsetX, toe.offsetY, toe.scaleX, toe.scaleY)</c>.
367 public Vector4 shoSegmentA
371 var sho
= parent
.m_Segments
[2];
372 return new Vector4(sho
.offsetX
, sho
.offsetY
, sho
.scaleX
, sho
.scaleY
);
377 /// A pre-built <see cref="Vector4"/> holding: <c>(sho.lnA, sho.B, 0, 0)</c>.
379 public Vector4 shoSegmentB
383 var sho
= parent
.m_Segments
[2];
384 return new Vector4(sho
.lnA
, sho
.B
, 0f
, 0f
);
390 /// The builtin <see cref="Uniforms"/> instance for this curve.
392 public readonly Uniforms uniforms
;