3Dの箱を背景写真の机の上に置く:置き場所の計算、影、うつり込み、採点のぶれ

3Dの箱を背景写真の机の上に置くため、写真の「物を置ける面」に合わせる計算、影の2層化、うつり込み、自動の揺れの廃止、袋が紙に見える問題への対処を記録します。見た目の採点が±0.3ぶれるので前後比較にした経緯つきです。

3Dの箱を背景写真の机の上に置く:置き場所の計算、影、うつり込み、採点のぶれ
目次
  1. 何を作りたかったか
  2. 最初の版:5〜6.5点
  3. 影を2層にして、うつり込みを足す
  4. 「写真の置ける面」に合わせる計算
  5. 自動の揺れをやめる
  6. パックが「ペラペラ」に見える
  7. 採点が±0.3ぶれる
  8. 残った課題
  9. まとめ

カードのパックを入れた3Dの箱を、背景写真の机やカウンターの上に置いて見せる画面を作りました。難しかったのは「そこに置いてあるように見せる」ことです。写真の中の置ける面に箱の位置・大きさ・角度を合わせる計算、影、うつり込み、そして袋が紙に見える問題を順に直しました。見た目の採点が回ごとにぶれるので、途中から前後比較で測るようにしています。

  • 箱の置き場所は、写真の「物を置ける面」の位置・大きさ・見下ろす角度から計算する
  • 影は、箱の底に沿う濃い影と、手前へ長い薄い影の2層にした
  • 何もしていない時は動かさない。回した時と絵の読み込み時だけ描き直す
  • 袋が紙に見えたのは、光沢の値の退行と、平らな積み方の2つが原因だった

何を作りたかったか

カードパックの箱を3Dで見せる画面はもともとありましたが、背景は無地でした。これを、お店のカウンター、カフェのカウンター、自分の机、という場面の写真の上に置いて、リアリティを出したい、というのが依頼です。場所は選べるようにして、最初にカードショップの場面で質を上げ、そのあとに他の2場面へ広げることにしました。

実装は three.js で、箱の形とパックの並べ方は既存の3D箱の部品と同じです。背景は、箱を置く場所を空けた写真(縦長・横長の2種類)です。

最初の版:5〜6.5点

最初の版は、背景写真を画面いっぱいに敷き、その上に3Dの開いた箱を置いて、視点は固定(立って少し見下ろす)、光は背景に合わせて暖色、足元にぼかした影、パックを箱から取り出す動き、という構成でした。

前後を並べて比べてもらうと、毎回「後が良い」になりました。リアリティは 5 から 6〜6.5(10点満点)まで上がりましたが、残る指摘が4つありました。

  • 足元の影の質(黒い板に見える、しみに見える、無い、を行き来した)
  • 箱が背景より明るく浮いて見える
  • パックの絵柄が淡い
  • ガラスへのうつり込みが無い(薄い写しは灰色のもやに見えたので外した)

影を2層にして、うつり込みを足す

影は、箱の底に沿う濃い影と、手前へ長い薄い影の2層にしました。光を左上の奥からにして、影が手前へ落ちるようにしたのがポイントです。見下ろす角度が浅いので、手前に伸びない影は画面でほとんど見えないからです。

箱の底に沿う濃い影は、キャンバスにぼかしを2回かけて作った絵を、床の面に貼っています。

const bc = document.createElement('canvas'); bc.width = bc.height = 256;
const bx = bc.getContext('2d');
bx.filter = 'blur(16px)'; bx.fillStyle = 'rgba(0,0,0,.5)'; bx.fillRect(58, 40, 140, 150);
bx.filter = 'blur(4px)';  bx.fillStyle = 'rgba(0,0,0,.9)'; bx.fillRect(62, 44, 132, 126);
// 広くて薄い影の上に、小さくて濃い影を重ねる → 箱の底にぴたりと沿う

広くて薄いぼかしの上に、小さくて濃いぼかしを重ねているので、箱の底の際は濃く、少し離れると薄くなります。手前へ長い薄い影は、光源から落ちる影(three.js のシャドウマップ)を、影だけを受ける透明な面に落として作っています。

うつり込みは、箱の正面の絵だけをさかさにして、足元に薄く貼る方法にしました。下へ行くほど消えるグラデーションを、透明度のマスクに使っています。最初は箱全体をさかさに写しましたが、灰色の箱が下へ伸びて見えたので没にしました。

このほかに、下の暗いぼかしが足元を覆っていたのを、ボタンのあたりだけにしました。箱の色は、暖色をかけすぎると濁るので控えめにし、濃さと明暗差は少し上げています。

この段階の絶対点は 5〜6.5 を行き来しました。うつり込みが効いている、効いていない、影が板に見える、弱い、という評価が、回ごとに割れたからです。

「写真の置ける面」に合わせる計算

次に、「何もしていないのに箱が揺れる」「机からはみ出して置かれている」という指摘を受けました。揺れは後で書きます。はみ出しは、背景の写真の中の置き場所と、箱の位置が合っていなかったのが原因です。

そこで、背景の写真の「物を置ける面」の位置を、別のエージェントに測らせ、その位置・大きさ・見下ろす角度に箱が来るように計算する形に作り直しました。場面ごとの値は、写真に対する割合で持ちます。

shop: { /* 光・影の設定は省略 */
  portrait:  { src: 'img/scene/a_portrait.jpg',  w: 1056, h: 1408,
               ax: .46, ay: .705, bw: .23, pitch: .26 },
  landscape: { src: 'img/scene/a_landscape.jpg', w: 1536, h: 1024,
               ax: .47, ay: .685, bw: .19, pitch: .15 } }
// ax, ay = 箱の底の中心 / bw = 箱の幅(どれも写真に対する割合)/ pitch = 見下ろす角度

背景の写真は「画面を覆う大きさ・下端そろえ」で敷きます。同じ計算で、写真の中の点が画面のどこに来るかを出します。

const s  = Math.max(W / o.w, H / o.h);        // 画面を覆う倍率
const iw = o.w * s, ih = o.h * s;
const px = (W - iw) / 2 + o.ax * iw;           // 置き場所の画面上の x
const py = H - ih + o.ay * ih;                 // 置き場所の画面上の y(下端そろえ)
const boxPx = o.bw * iw;                       // 箱の幅が画面で何pxか
const depth = 150 * (H / 2) / (T2 * boxPx);    // その幅に見える、カメラまでの距離

箱の幅は3D空間で150の単位です。それが画面で boxPx に見える距離を、カメラの視野角の式から求めて depth にします。そのあと、見下ろす角度 pitch に合わせてカメラの高さを決め、箱の底の中心が (px, py) に来るようにします。この部分は、カメラの位置と距離が互いに影響するので、3回の繰り返しで値を収束させています。画面の縦横が変わっても、写真の中の置き場所に箱が付いてくるのは、この計算のおかげです。

確かめは、3つの場面(カードショップ、カフェのカウンター、自分の机)をスマホ幅とパソコン幅で見て、はみ出し、値札の重なり、ボタンとの重なりが無いことでした。これは別のエージェントの目視です。リアリティは 5〜7(10点満点)に収まりました。

自動の揺れをやめる

「何もしていないのに箱がゆらゆら動く」ことへの対処は、自動の揺れをやめて、描き直すのを必要な時だけにすることでした。

let dirty = true;
const loop = () => {
  if (disposed) return;
  if (dirty) { dirty = false; draw(); }
  loopId = requestAnimationFrame(loop);
};
// 回した時(pointermove)と、絵の読み込み完了時、画面の大きさが変わった時に dirty = true

何もしていない時は描かない、という設計です。止まっていることは、2つの時点の画素を比較して確かめました。

あわせて、手前にばらで置いていた1パックは、薄い板に見えたのでやめました。値札(NEW/限定パック名/1パック5枚入り)を箱の右横に置き、ふたの内側(店頭のPOP)は、絵を少し光らせて、正面から弱い光を当てるようにしました。

パックが「ペラペラ」に見える

次の指摘は、箱の中のパックがペラペラな印象になる、というものでした。原因は2つありました。

  1. パックのアルミの照り返しの強さ(envMapIntensity)を 0.3、粗さ 0.45 以上、クリアコート 0 にしていた。「箱が背景から浮く」対策の副作用です。以前に、別の画面で「低すぎると光沢が消えて紙に見えた」と直した 2.2 から退行していました。
  2. 箱の中は15段に平らに積んであり、低い視点からは一番上の表面が細く見えるだけで、厚みも封のギザギザも見えなかった

直しは次のとおりです。

for (const surface of Object.values(pouch.materials))
  surface.envMapIntensity = 2.2;   // パックのアルミの照り返し。箱の紙・ふたは低いまま
// ふくらみ強め: item.scale.z = 1.8
// 左右の列の一番上だけ、奥を少し持ち上げる
const lift = col ? .2 : .12;       // ラジアン
item.rotation.x += lift;
item.position.y += 50.5 * Math.sin(lift) + 1.2;

照り返しを 2.2 に戻し、ふくらみを 1.8 倍にし、左右の列の一番上だけ奥を持ち上げています。左右で角度を変えて、そろいすぎないようにしました。箱の浮きは、箱の紙とふた側の値で抑えます。

効果は、前後の画面を scripts/shoot_box.py で撮って比べました。光沢だけを直した段階では、「わずかに良い」止まりでした。持ち上げを足して、封と光沢が見えるようになり、袋に見えるようになりました。直す前の版は、別名で保管してあります。

背景写真の机の場面。箱を置く場所が空けてある

上の画像は、背景として使う場面の1枚です。中央の空いた面に、ここまでの計算で箱を置きます。

採点が±0.3ぶれる

ここまでの間、見た目の採点は別のエージェントに依頼していました。同じ基準で採点しても、回ごとに ±0.3 ほどぶれます。カード本体の採点でも 6.7 → 7.0 → 6.8 のように、直した内容に対して上下しました。ぶれの原因は調べていません。

そのため、点で判断するのをやめて、「前の版と並べて、どちらが良いか」を聞く形に切り替えました。前の画面を残しておいて、新旧を並べる運用です。この前後比較は、毎回「後が良い」になっていて、直した内容が効いているかを確かめるには、点よりも信頼できると考えています。ただし、絶対点が 5〜7 のあたりに留まっていることには変わりありません。

残った課題

  • 値札の影の向き
  • 机の天板とマットの境目
  • 箱の足元の影の弱さ
  • 絶対的なリアリティは 5〜7(10点満点)にとどまる

まとめ

箱を写真の机に置くには、写真の中の置き場所を数値で測り、同じ計算で画面に変えるのが基本でした。そのうえで、影は2層、うつり込みは正面の絵だけ、揺れは止める、光沢の値は退行させない、と1つずつ直しました。採点のぶれが大きい時は、点でなく前後比較にする、というのが一番の学びです。

この画面に載せるカードの表現は 紙のカードにできない表現の記事、ゲームの数値調整は バランス調整の記事 をご覧ください。