光の向きで、表面の明るさが決まる
シェーディングとは、物の表面の明るさを、光の向きから計算して、立体に見せることです。同じ球でも、光が当たる側は明るく、反対側は暗くなります。
このレッスンでは、法線と光の向きの角度から、拡散反射の明るさを求めます。
このレッスンのゴール
メモ
法線と光の向きから、拡散反射の明るさを計算できる。
キーワード
| ことば | いみ |
|---|---|
| 法線 | 表面に垂直に立つ矢印 |
| 単位ベクトル | 長さが 1 の矢印 |
| 正規化 | 矢印の長さを 1 にそろえること |
| 拡散反射 | 光が表面で、いろいろな向きに散る反射 |
| 内積 | 対応する数をかけて、足した値 |
| 光の向き | 表面の点から光源へ向かう向き |
学生が入力するセルは、このプレビューでは空白です。
法線は、表面に垂直な向きの矢印
法線は、表面の1点で、表面に垂直に立つ矢印です。球では、中心からその点へ向かう向きが法線になります。
点 p の法線は、(p − 中心) を半径で割った矢印で、長さは 1 です。長さが 1 の矢印を、単位ベクトルと呼びます。
中心 (0, 0, 0)、半径 5 の球の法線
| 球の上の点 p | (p − 中心) ÷ 半径 | 法線 |
|---|---|---|
| (5, 0, 0) | (5, 0, 0) ÷ 5 | (1, 0, 0) |
| (0, 3, 4) | (0, 3, 4) ÷ 5 | (0, 0.6, 0.8) |
| (0, 0, -5) | (0, 0, -5) ÷ 5 | (0, 0, -1) |
学生が入力するセルは、このプレビューでは空白です。
長さを 1 にそろえるには、長さで割る
ベクトルの長さは、x、y、z の2乗を足して、平方根をとった値です。(3, 0, 4) の長さは、9 + 16 の平方根で 5 です。
長さを 1 にするには、各数を長さで割ります。(3, 0, 4) は (0.6, 0, 0.8) になります。この操作を、正規化と呼びます。
拡散反射は、法線と光の向きの内積で決まる
拡散反射の明るさは、光が表面にどれだけ正面から当たるかで決まります。法線と光の向きが同じなら最も明るく、直角なら暗くなります。
2つを単位ベクトルにそろえて内積をとると、その値が 0 から 1 の明るさの割合です。負になったら、光が裏から当たっているので 0 にします。
法線と光の角度と、明るさの関係
| 角度 | 内積 | 明るさ (最大 255) |
|---|---|---|
| 0 度 | 1.0 | 255 |
| 60 度 | 0.5 | 約 128 |
| 90 度 | 0 | 0 |
| 120 度 | -0.5 を 0 にする | 0 |
学生が入力するセルは、このプレビューでは空白です。
内積から明るさを求める
normal = [0, 1, 0]
light = [0, 5, 12]
length = (light[0] ** 2 + light[1] ** 2 + light[2] ** 2) ** 0.5
unit_light = [light[0] / length, light[1] / length, light[2] / length]
cosine = normal[0] * unit_light[0] + normal[1] * unit_light[1] + normal[2] * unit_light[2]
if cosine < 0:
cosine = 0
print(round(cosine, 2), round(255 * cosine))
# light を [0, -5, 12] に変えて、もう一度実行します。
# 明るさは、いくつになりますか。
numpy で内積と明るさを確かめる
次のコマンドは、法線 (0, 0, 1) と光の向き (3, 0, 4) で、内積と明るさを求めます。光の向きを 5 で割って、長さを 1 にそろえてから、内積をとります。
出力の 0.8 は内積です。204 は、255 に 0.8 をかけて四捨五入した明るさです。
内積 0.8 の明るさを求める
docker exec jupyter python3 -c "import numpy as np; n = np.array([0, 0, 1]); l = np.array([3, 0, 4]) / 5; print(n @ l, round(float(255 * max(0, n @ l))))"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
球の1点の明るさは、3つの手順で求まる
球の1点の明るさは、3つの手順で求まります。法線を求め、光の向きと一緒に正規化し、内積から明るさを出します。
レイが球に当たった点に、この手順をくり返すと、すべてのピクセルの明るさが決まります。当たらなかったピクセルは、背景の色です。
光を当てた球を、画像にする
次のコマンドは、半径 50 の球に、光の向き (-1, -1, 2) から光を当て、灰色の画像 sphere.png に保存します。画面では y が下向きなので、光は左上の手前から来ます。
sphere.png を開くと、黒い地に球が浮かび、左上がいちばん明るく、右下の縁は暗くなって、背景の黒に溶けます。表示は、中心、左上寄りの (20, 20)、右下寄りの (80, 80)、角の (0, 0) の明るさです。
光を当てた球を sphere.png に保存
docker exec jupyter python3 -c "import numpy as np; from PIL import Image; y, x = np.mgrid[-50:50, -50:50]; rest = 2500 - x * x - y * y; z = np.sqrt(np.maximum(rest, 0)); light = np.array([-1, -1, 2]) / np.sqrt(6); level = np.maximum(0, (x * light[0] + y * light[1] + z * light[2]) / 50); picture = np.where(rest > 0, level * 255, 0).astype('uint8'); Image.fromarray(picture).save('sphere.png'); print(picture[50, 50], picture[20, 20], picture[80, 80], picture[0, 0])"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
光に近いほど明るくなると考えがちです。拡散反射は、距離ではなく、法線と光の向きの角度で決まります。向きを正規化せずに内積をとると、255 を超えるなど、正しくない明るさになります。