回答編集履歴

3

カラーピッカー側の値を矯正しようとした場合を追記

2022/03/02 12:36

投稿

Bongo
Bongo

スコア10816

test CHANGED
@@ -9,31 +9,31 @@
9
9
  以前別の方の「[Unity マテリアルのEmission intensityについて](https://teratail.com/questions/291815)」とのご質問に回答しました際に調べたところによると、どうやらこのHDRカラーの分解は[ColorMutator](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/GUI/ColorMutator.cs)が行っているようでしたが、下記引用のように...
10
10
 
11
11
  > ```C#
12
- > internal static void DecomposeHdrColor(Color linearColorHdr, out Color32 baseLinearColor, out float exposure)
12
+ > internal static void DecomposeHdrColor(Color linearColorHdr, out Color32 baseLinearColor, out float exposure)
13
- > {
13
+ > {
14
- > baseLinearColor = linearColorHdr;
14
+ > baseLinearColor = linearColorHdr;
15
- > var maxColorComponent = linearColorHdr.maxColorComponent;
15
+ > var maxColorComponent = linearColorHdr.maxColorComponent;
16
- > // replicate Photoshops's decomposition behaviour
16
+ > // replicate Photoshops's decomposition behaviour
17
- > if (maxColorComponent == 0f || maxColorComponent <= 1f && maxColorComponent >= 1 / 255f)
17
+ > if (maxColorComponent == 0f || maxColorComponent <= 1f && maxColorComponent >= 1 / 255f)
18
- > {
18
+ > {
19
- > exposure = 0f;
19
+ > exposure = 0f;
20
20
  >
21
- > baseLinearColor.r = (byte)Mathf.RoundToInt(linearColorHdr.r * 255f);
21
+ > baseLinearColor.r = (byte)Mathf.RoundToInt(linearColorHdr.r * 255f);
22
- > baseLinearColor.g = (byte)Mathf.RoundToInt(linearColorHdr.g * 255f);
22
+ > baseLinearColor.g = (byte)Mathf.RoundToInt(linearColorHdr.g * 255f);
23
- > baseLinearColor.b = (byte)Mathf.RoundToInt(linearColorHdr.b * 255f);
23
+ > baseLinearColor.b = (byte)Mathf.RoundToInt(linearColorHdr.b * 255f);
24
- > }
24
+ > }
25
- > else
25
+ > else
26
- > {
26
+ > {
27
- > // calibrate exposure to the max float color component
27
+ > // calibrate exposure to the max float color component
28
- > var scaleFactor = k_MaxByteForOverexposedColor / maxColorComponent;
28
+ > var scaleFactor = k_MaxByteForOverexposedColor / maxColorComponent;
29
- > exposure = Mathf.Log(255f / scaleFactor) / Mathf.Log(2f);
29
+ > exposure = Mathf.Log(255f / scaleFactor) / Mathf.Log(2f);
30
30
  >
31
- > // maintain maximal integrity of byte values to prevent off-by-one errors when scaling up a color one component at a time
31
+ > // maintain maximal integrity of byte values to prevent off-by-one errors when scaling up a color one component at a time
32
- > baseLinearColor.r = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.r));
32
+ > baseLinearColor.r = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.r));
33
- > baseLinearColor.g = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.g));
33
+ > baseLinearColor.g = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.g));
34
- > baseLinearColor.b = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.b));
34
+ > baseLinearColor.b = Math.Min(k_MaxByteForOverexposedColor, (byte)Mathf.CeilToInt(scaleFactor * linearColorHdr.b));
35
- > }
36
- > }
35
+ > }
36
+ > }
37
37
  > ```
38
38
 
39
39
  入力されたHDRカラーがLDRカラーで表現できない(明度が1/255を下回っているか、1を超えている)場合、元のHDRカラーの明度が191/255に(`k_MaxByteForOverexposedColor`として定義されている値に)なるようスケーリングしたものをベースカラーとして決めて、それに合わせたIntensityを計算しているらしいですね。
@@ -50,67 +50,67 @@
50
50
  [RequireComponent(typeof(Renderer))]
51
51
  public class EmissionController : MonoBehaviour
52
52
  {
53
- private static readonly int EmissionColorProperty = Shader.PropertyToID("_EmissionColor");
54
-
55
- [ColorUsage(false)] public Color ColorCode = Color.black;
56
- public float intensityValue;
57
-
58
- private Color previousColorCode;
59
- private float previousIntensityValue = float.NaN;
60
- private new Renderer renderer;
61
-
62
- private void Update()
63
- {
64
- if (this.renderer == null)
65
- {
66
- this.renderer = this.GetComponent<Renderer>();
67
- }
68
-
69
- // ColorCodeもintensityValueも前フレームから変わっていなければ
70
- // Emission更新の必要はないと見なして何もしない
71
- if ((this.ColorCode == this.previousColorCode) && (this.intensityValue == this.previousIntensityValue))
72
- {
73
- return;
74
- }
75
-
76
- // まずColorCodeをintensityValueに応じて増幅し、Emissionにセットするべき色を決める
77
- var hdrColor = this.ColorCode * Mathf.Pow(2.0f, this.intensityValue);
78
- hdrColor.a = this.ColorCode.a;
79
-
80
- // EmissionにhdrColorをセットする
81
- var material = this.renderer.material;
82
- if (material != null)
83
- {
84
- material.EnableKeyword("_EMISSION");
85
- material.SetColor(EmissionColorProperty, hdrColor);
86
- }
87
-
88
- // そしてhdrColorをベースカラーとIntensityに分解する
89
- // まずhdrColorのRGBのうち最も大きい成分の値を取得する
90
- var maxComponent = hdrColor.maxColorComponent;
91
-
92
- // intensityValueはひとまず0にする
93
- this.intensityValue = 0.0f;
94
-
95
- // その最大成分が0なら完全な黒なのでLDRで表現でき、分解の必要はない
96
- // また、1/255以上1以下ならLDRで表現でき、分解の必要はない
97
- if ((maxComponent != 0.0f) && ((maxComponent < (1.0f / 255.0f)) || (maxComponent > 1.0f)))
98
- {
99
- // そうでないならば、まずmaxComponentに対する191/255の倍率を求める
100
- var scale = 191.0f / (maxComponent * 255.0f);
101
-
102
- // そしてそのスケールを使ってColorCodeのRGBを矯正する
103
- this.ColorCode = hdrColor * scale;
104
- this.ColorCode.a = hdrColor.a;
105
-
106
- // また、scaleの逆数の2を底とする対数を求め、intensityValueとする
107
- this.intensityValue = Mathf.Log(1.0f / scale, 2.0f);
108
- }
109
-
110
- // 最後に、次のフレームに備えて現在のColorCodeとintensityValueを保存しておく
111
- this.previousColorCode = this.ColorCode;
112
- this.previousIntensityValue = this.intensityValue;
113
- }
53
+ private static readonly int EmissionColorProperty = Shader.PropertyToID("_EmissionColor");
54
+
55
+ [ColorUsage(false)] public Color ColorCode = Color.black;
56
+ public float intensityValue;
57
+
58
+ private Color previousColorCode;
59
+ private float previousIntensityValue = float.NaN;
60
+ private new Renderer renderer;
61
+
62
+ private void Update()
63
+ {
64
+ if (this.renderer == null)
65
+ {
66
+ this.renderer = this.GetComponent<Renderer>();
67
+ }
68
+
69
+ // ColorCodeもintensityValueも前フレームから変わっていなければ
70
+ // Emission更新の必要はないと見なして何もしない
71
+ if ((this.ColorCode == this.previousColorCode) && (this.intensityValue == this.previousIntensityValue))
72
+ {
73
+ return;
74
+ }
75
+
76
+ // まずColorCodeをintensityValueに応じて増幅し、Emissionにセットするべき色を決める
77
+ var hdrColor = this.ColorCode * Mathf.Pow(2.0f, this.intensityValue);
78
+ hdrColor.a = this.ColorCode.a;
79
+
80
+ // EmissionにhdrColorをセットする
81
+ var material = this.renderer.material;
82
+ if (material != null)
83
+ {
84
+ material.EnableKeyword("_EMISSION");
85
+ material.SetColor(EmissionColorProperty, hdrColor);
86
+ }
87
+
88
+ // そしてhdrColorをベースカラーとIntensityに分解する
89
+ // まずhdrColorのRGBのうち最も大きい成分の値を取得する
90
+ var maxComponent = hdrColor.maxColorComponent;
91
+
92
+ // intensityValueはひとまず0にする
93
+ this.intensityValue = 0.0f;
94
+
95
+ // その最大成分が0なら完全な黒なのでLDRで表現でき、分解の必要はない
96
+ // また、1/255以上1以下ならLDRで表現でき、分解の必要はない
97
+ if ((maxComponent != 0.0f) && ((maxComponent < (1.0f / 255.0f)) || (maxComponent > 1.0f)))
98
+ {
99
+ // そうでないならば、まずmaxComponentに対する191/255の倍率を求める
100
+ var scale = 191.0f / (maxComponent * 255.0f);
101
+
102
+ // そしてそのスケールを使ってColorCodeのRGBを矯正する
103
+ this.ColorCode = hdrColor * scale;
104
+ this.ColorCode.a = hdrColor.a;
105
+
106
+ // また、scaleの逆数の2を底とする対数を求め、intensityValueとする
107
+ this.intensityValue = Mathf.Log(1.0f / scale, 2.0f);
108
+ }
109
+
110
+ // 最後に、次のフレームに備えて現在のColorCodeとintensityValueを保存しておく
111
+ this.previousColorCode = this.ColorCode;
112
+ this.previousIntensityValue = this.intensityValue;
113
+ }
114
114
  }
115
115
  ```
116
116
 
@@ -118,3 +118,140 @@
118
118
  そして、マテリアル側のEmissionもスクリプト側と同じ設定値になりました。
119
119
 
120
120
  ![図](https://ddjkaamml8q8x.cloudfront.net/questions/2022-02-22/d5323cf8-2d63-4e9c-8c7e-e6b396bb6dec.gif)
121
+
122
+ ## カラーピッカー側の値を矯正しようとするとどうなるか
123
+
124
+ たとえばスクリプトを下記のようなものに変更した場合(なんだかマルウェア的な変なことをしていて気持ち悪いかもしれませんが、あくまでも実験ということでご容赦ください)...
125
+
126
+ ```C#
127
+ using System;
128
+ using System.Reflection;
129
+ using System.Runtime.CompilerServices;
130
+ using System.Runtime.InteropServices;
131
+ using UnityEngine;
132
+ #if UNITY_EDITOR
133
+ using UnityEditor;
134
+ #endif
135
+
136
+ [RequireComponent(typeof(Renderer))]
137
+ public class EmissionController2 : MonoBehaviour
138
+ {
139
+ private static readonly int EmissionColorProperty = Shader.PropertyToID("_EmissionColor");
140
+
141
+ [ColorUsage(false)] public Color ColorCode = Color.black;
142
+ public float intensityValue;
143
+
144
+ private readonly byte[] decomposeHdrColorBackup = new byte[5];
145
+ private IntPtr decomposeHdrColorPointer;
146
+ private Color previousColorCode;
147
+ private float previousIntensityValue = float.NaN;
148
+ private new Renderer renderer;
149
+
150
+ private void Update()
151
+ {
152
+ if (this.renderer == null)
153
+ {
154
+ this.renderer = this.GetComponent<Renderer>();
155
+ }
156
+
157
+ // ColorCodeもintensityValueも前フレームから変わっていなければ
158
+ // Emission更新の必要はないと見なして何もしない
159
+ if ((this.ColorCode == this.previousColorCode) && (this.intensityValue == this.previousIntensityValue))
160
+ {
161
+ return;
162
+ }
163
+
164
+ // まずColorCodeをintensityValueに応じて増幅し、Emissionにセットするべき色を決める
165
+ var hdrColor = this.ColorCode * Mathf.Pow(2.0f, this.intensityValue);
166
+ hdrColor.a = this.ColorCode.a;
167
+
168
+ // EmissionにhdrColorをセットする
169
+ var material = this.renderer.material;
170
+ if (material != null)
171
+ {
172
+ material.EnableKeyword("_EMISSION");
173
+ material.SetColor(EmissionColorProperty, hdrColor);
174
+ }
175
+
176
+ // ColorMutator.DecomposeHdrColorを乗っ取り、代わりにDecomposeHdrColor255を実行させるようにする
177
+ this.HijackDecomposeHdrColor();
178
+
179
+ // 最後に、次のフレームに備えて現在のColorCodeとintensityValueを保存しておく
180
+ this.previousColorCode = this.ColorCode;
181
+ this.previousIntensityValue = this.intensityValue;
182
+ }
183
+
184
+ // コンポーネント破壊時にColorMutator.DecomposeHdrColorを元の状態に戻す
185
+ private void OnDestroy()
186
+ {
187
+ if (this.decomposeHdrColorPointer == IntPtr.Zero)
188
+ {
189
+ return;
190
+ }
191
+ Marshal.Copy(this.decomposeHdrColorBackup, 0, this.decomposeHdrColorPointer, this.decomposeHdrColorBackup.Length);
192
+ this.decomposeHdrColorPointer = IntPtr.Zero;
193
+ }
194
+
195
+ // ColorMutator.DecomposeHdrColorを乗っ取り、DecomposeHdrColor255へ制御を移すよう改変を加える
196
+ private void HijackDecomposeHdrColor()
197
+ {
198
+ #if UNITY_EDITOR
199
+ if (this.decomposeHdrColorPointer != IntPtr.Zero)
200
+ {
201
+ return;
202
+ }
203
+
204
+ // ColorMutator.DecomposeHdrColorのメモリ上の位置を取得する
205
+ const BindingFlags bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Static;
206
+ var decomposeHdrColorMethodHandle = typeof(EditorWindow).Assembly.GetType("UnityEditor.ColorMutator").GetMethod("DecomposeHdrColor", bindingFlags).MethodHandle;
207
+ RuntimeHelpers.PrepareMethod(decomposeHdrColorMethodHandle);
208
+ this.decomposeHdrColorPointer = decomposeHdrColorMethodHandle.GetFunctionPointer();
209
+
210
+ // DecomposeHdrColor255のメモリ上の位置を取得する
211
+ var decomposeHdrColor255MethodHandle = typeof(EmissionController2).GetMethod(nameof(DecomposeHdrColor255), bindingFlags).MethodHandle;
212
+ RuntimeHelpers.PrepareMethod(decomposeHdrColor255MethodHandle);
213
+ var decomposeHdrColor255Pointer = decomposeHdrColor255MethodHandle.GetFunctionPointer();
214
+
215
+ // コンポーネント破壊時にColorMutator.DecomposeHdrColorを復元するため、先頭5バイトをバックアップしておく
216
+ Marshal.Copy(this.decomposeHdrColorPointer, this.decomposeHdrColorBackup, 0, this.decomposeHdrColorBackup.Length);
217
+
218
+ // ColorMutator.DecomposeHdrColorの先頭5バイトにDecomposeHdrColor255へジャンプするコードを埋め込む
219
+ Marshal.WriteByte(this.decomposeHdrColorPointer, 0xE9);
220
+ Marshal.WriteInt32(this.decomposeHdrColorPointer, 1, (int)decomposeHdrColor255Pointer - (int)this.decomposeHdrColorPointer - this.decomposeHdrColorBackup.Length);
221
+ #endif
222
+ }
223
+
224
+ // オリジナルのColorMutator.DecomposeHdrColorはベースカラーの基準値が191だが、
225
+ // こちらは255を基準にベースカラーを決める
226
+ private static void DecomposeHdrColor255(Color linearColorHdr, out Color32 baseLinearColor, out float exposure)
227
+ {
228
+ baseLinearColor = linearColorHdr;
229
+ var maxColorComponent = linearColorHdr.maxColorComponent;
230
+ if (maxColorComponent is 0.0f or <= 1.0f and >= 1.0f / 255.0f)
231
+ {
232
+ exposure = 0.0f;
233
+ baseLinearColor.r = (byte)Mathf.RoundToInt(linearColorHdr.r * 255.0f);
234
+ baseLinearColor.g = (byte)Mathf.RoundToInt(linearColorHdr.g * 255.0f);
235
+ baseLinearColor.b = (byte)Mathf.RoundToInt(linearColorHdr.b * 255.0f);
236
+ }
237
+ else
238
+ {
239
+ var scaleFactor = 255.0f / maxColorComponent;
240
+ exposure = Mathf.Log(maxColorComponent, 2.0f);
241
+ baseLinearColor.r = (byte)Math.Min(255, Mathf.CeilToInt(scaleFactor * linearColorHdr.r));
242
+ baseLinearColor.g = (byte)Math.Min(255, Mathf.CeilToInt(scaleFactor * linearColorHdr.g));
243
+ baseLinearColor.b = (byte)Math.Min(255, Mathf.CeilToInt(scaleFactor * linearColorHdr.b));
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ マテリアル上でもIntensityが2になりました。
250
+
251
+ ![図](https://ddjkaamml8q8x.cloudfront.net/questions/2022-03-02/da5766df-62c4-460a-ace7-c9bf0bd1b2ac.gif)
252
+
253
+ 一見うまくいったように見えるかもしれませんが、逆にスクリプト側のベースカラー輝度が191でも、マテリアル側のベースカラー輝度は255になってしまいます。
254
+
255
+ ![図](https://ddjkaamml8q8x.cloudfront.net/questions/2022-03-02/3d8ac01f-03a4-4146-9942-64ee7cd5df2e.gif)
256
+
257
+ (255, 128, 64)、Intensity 2の色と(191, 96, 48)、Intensity 2.416924の色は計算誤差を無視すれば同等で区別できず、カラーピッカー側で本来のベースカラー・Intensityを復元するのは無理っぽいです...

2

スクリプト側の値を矯正する例を追記

2022/02/22 13:45

投稿

Bongo
Bongo

スコア10816

test CHANGED
@@ -39,3 +39,82 @@
39
39
  入力されたHDRカラーがLDRカラーで表現できない(明度が1/255を下回っているか、1を超えている)場合、元のHDRカラーの明度が191/255に(`k_MaxByteForOverexposedColor`として定義されている値に)なるようスケーリングしたものをベースカラーとして決めて、それに合わせたIntensityを計算しているらしいですね。
40
40
 
41
41
  カラーピッカー上の値とスクリプト上の値の不整合を解消するとなると、おそらくスクリプト側の値の方をカラーピッカー側の値に合わせる...つまり`ColorMutator`と同じ計算式で色を矯正して、`ColorCode`と`intensityValue`の方をカラーピッカーと一致するよう書き換えることになるんじゃないかと思います。
42
+
43
+ ## スクリプト側の値を矯正する例
44
+
45
+ 下記のようなスクリプトをオブジェクトにアタッチして...
46
+
47
+ ```C#
48
+ using UnityEngine;
49
+
50
+ [RequireComponent(typeof(Renderer))]
51
+ public class EmissionController : MonoBehaviour
52
+ {
53
+ private static readonly int EmissionColorProperty = Shader.PropertyToID("_EmissionColor");
54
+
55
+ [ColorUsage(false)] public Color ColorCode = Color.black;
56
+ public float intensityValue;
57
+
58
+ private Color previousColorCode;
59
+ private float previousIntensityValue = float.NaN;
60
+ private new Renderer renderer;
61
+
62
+ private void Update()
63
+ {
64
+ if (this.renderer == null)
65
+ {
66
+ this.renderer = this.GetComponent<Renderer>();
67
+ }
68
+
69
+ // ColorCodeもintensityValueも前フレームから変わっていなければ
70
+ // Emission更新の必要はないと見なして何もしない
71
+ if ((this.ColorCode == this.previousColorCode) && (this.intensityValue == this.previousIntensityValue))
72
+ {
73
+ return;
74
+ }
75
+
76
+ // まずColorCodeをintensityValueに応じて増幅し、Emissionにセットするべき色を決める
77
+ var hdrColor = this.ColorCode * Mathf.Pow(2.0f, this.intensityValue);
78
+ hdrColor.a = this.ColorCode.a;
79
+
80
+ // EmissionにhdrColorをセットする
81
+ var material = this.renderer.material;
82
+ if (material != null)
83
+ {
84
+ material.EnableKeyword("_EMISSION");
85
+ material.SetColor(EmissionColorProperty, hdrColor);
86
+ }
87
+
88
+ // そしてhdrColorをベースカラーとIntensityに分解する
89
+ // まずhdrColorのRGBのうち最も大きい成分の値を取得する
90
+ var maxComponent = hdrColor.maxColorComponent;
91
+
92
+ // intensityValueはひとまず0にする
93
+ this.intensityValue = 0.0f;
94
+
95
+ // その最大成分が0なら完全な黒なのでLDRで表現でき、分解の必要はない
96
+ // また、1/255以上1以下ならLDRで表現でき、分解の必要はない
97
+ if ((maxComponent != 0.0f) && ((maxComponent < (1.0f / 255.0f)) || (maxComponent > 1.0f)))
98
+ {
99
+ // そうでないならば、まずmaxComponentに対する191/255の倍率を求める
100
+ var scale = 191.0f / (maxComponent * 255.0f);
101
+
102
+ // そしてそのスケールを使ってColorCodeのRGBを矯正する
103
+ this.ColorCode = hdrColor * scale;
104
+ this.ColorCode.a = hdrColor.a;
105
+
106
+ // また、scaleの逆数の2を底とする対数を求め、intensityValueとする
107
+ this.intensityValue = Mathf.Log(1.0f / scale, 2.0f);
108
+ }
109
+
110
+ // 最後に、次のフレームに備えて現在のColorCodeとintensityValueを保存しておく
111
+ this.previousColorCode = this.ColorCode;
112
+ this.previousIntensityValue = this.intensityValue;
113
+ }
114
+ }
115
+ ```
116
+
117
+ ゲーム実行時にスクリプト側の`ColorCode`と`intensityValue`を設定したところ、`ColorCode`は少し暗く、`intensityValue`は少し大きく変化しました。
118
+ そして、マテリアル側のEmissionもスクリプト側と同じ設定値になりました。
119
+
120
+ ![図](https://ddjkaamml8q8x.cloudfront.net/questions/2022-02-22/d5323cf8-2d63-4e9c-8c7e-e6b396bb6dec.gif)

1

「輝度」は「明度」の方が妥当なように思い直して修正

2022/02/21 18:39

投稿

Bongo
Bongo

スコア10816

test CHANGED
@@ -36,6 +36,6 @@
36
36
  > }
37
37
  > ```
38
38
 
39
- 入力されたHDRカラーがLDRカラーで表現できない(度が1/255を下回っているか、1を超えている)場合、元のHDRカラーの度が191/255に(`k_MaxByteForOverexposedColor`として定義されている値に)なるようスケーリングしたものをベースカラーとして決めて、それに合わせたIntensityを計算しているらしいですね。
39
+ 入力されたHDRカラーがLDRカラーで表現できない(度が1/255を下回っているか、1を超えている)場合、元のHDRカラーの度が191/255に(`k_MaxByteForOverexposedColor`として定義されている値に)なるようスケーリングしたものをベースカラーとして決めて、それに合わせたIntensityを計算しているらしいですね。
40
40
 
41
41
  カラーピッカー上の値とスクリプト上の値の不整合を解消するとなると、おそらくスクリプト側の値の方をカラーピッカー側の値に合わせる...つまり`ColorMutator`と同じ計算式で色を矯正して、`ColorCode`と`intensityValue`の方をカラーピッカーと一致するよう書き換えることになるんじゃないかと思います。