3Dの箱を背景写真の机の上に置く:置き場所の計算、影、うつり込み、採点のぶれ
に公開
3Dの箱を背景写真の机の上に置くため、写真の「物を置ける面」に合わせる計算、影の2層化、うつり込み、自動の揺れの廃止、袋が紙に見える問題への対処を記録します。見た目の採点が±0.3ぶれるので前後比較にした経緯つきです。
カードのパックを入れた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つありました。
- パックのアルミの照り返しの強さ(
envMapIntensity)を 0.3、粗さ 0.45 以上、クリアコート 0 にしていた。「箱が背景から浮く」対策の副作用です。以前に、別の画面で「低すぎると光沢が消えて紙に見えた」と直した 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つずつ直しました。採点のぶれが大きい時は、点でなく前後比較にする、というのが一番の学びです。
この画面に載せるカードの表現は 紙のカードにできない表現の記事、ゲームの数値調整は バランス調整の記事 をご覧ください。