Three.jsの影に同心円の縞が出る原因|シャドウアクネの直し方
Three.jsで作ったサイコロゲームで、丼(どんぶり)の内側に木目のような同心円の縞がずっと出ていました。丼にはテクスチャを1枚も貼っておらず、単色のマテリアルです。
結論から書くと、原因はシャドウアクネ(物体が自分自身に落とす影の計算で出る縞)でした。丼に影を落とさせないcastShadow = falseが、いちばんきれいに縞を消せました。shadow.biasやshadow.normalBiasでも縞は消えますが、縁のギザギザや内側の暗さと引き換えになります。影の解像度を上げるだけでは消えません。
この記事では、丼の設定を再現して設定ごとに描画した結果と、縞を消したあとに目立った白飛びの直し方をまとめます。
丼に同心円の縞が出る原因は何か
結論:丼が自分自身に影を落としていて、その影の計算の誤差が曲面の上で縞になっていました。これをシャドウアクネと呼びます。
ブラウザで遊べる3Dチンチロと同じ丼の設定を、three.js r128で再現した画像です。丼の内側に同心円の縞がはっきり出ています。
Three.jsの影は、ライトから見た奥行きを「シャドウマップ」という画像に記録し、描画する面の奥行きと比べて影かどうかを決めます。自分自身に影を落とす面では、この比較が記録の精度の限界でわずかにずれ、影の中と外が交互に判定されます。平面なら細かい模様で済みますが、丼のような曲面では同心円の縞として見えます。
再現に使った丼とライトの設定です。ライトはPointLightで、影は6方向のシャドウマップ(キューブシャドウマップ)で描かれます。
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const light1 = new THREE.PointLight(0xffffff, 1.0, 100);
light1.position.set(2, 6, 6);
light1.castShadow = true; // shadow.bias は既定の 0、shadow.mapSize は既定の 512
scene.add(light1);
const geometry = new THREE.SphereGeometry(2, 64, 64, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2);
const material = new THREE.MeshStandardMaterial({
color: 0xd2b48c, roughness: 0.4, metalness: 0.2,
side: THREE.DoubleSide, transparent: true, opacity: 0.95,
});
const bowl = new THREE.Mesh(geometry, material);
bowl.castShadow = true; // 丼が影を落とす
bowl.receiveShadow = true; // 丼が影を受ける
scene.add(bowl);
同じ物体がcastShadowとreceiveShadowの両方をtrueにしていて、shadow.biasが0のままという組み合わせが、シャドウアクネの条件です。
最新のthree.jsでも縞は出るか
結論:出ます。2026年9月時点の最新のr186でも、模様は細かい横縞に変わりましたが、縞は消えませんでした。
r155以降はライトの明るさの単位が変わっているので、明るさをMath.PI倍にし、PointLightの減衰(decay)を0にして、r128に近い見た目で比べました。縞の模様は変わっても、原因と直し方は同じです。
設定ごとに縞はどう変わるか
結論:丼のcastShadowをfalseにするのが、見た目を変えずに縞を消せる唯一の設定でした。
r128で、設定を1つずつ変えて描画した結果です。
| 変えた設定 | 縞 | ほかの変化 |
|---|---|---|
shadow.mapSizeを2048に(解像度だけ上げる) |
残る | 縞が細かくなるだけ |
shadow.bias = -0.0015、mapSizeを1024に |
消える | 丼の右下の縁に階段状のギザギザ |
shadow.normalBias = 0.02 |
消える | 丼の内側全体が暗くなる |
shadow.normalBias = 0.05 |
消える | 内側が暗く、右の縁にギザギザ |
丼のcastShadow = false |
消える | なし(サイコロの影は丼に落ちる) |
解像度を上げても縞が消えない理由
結論:シャドウマップの解像度を上げると、1つの縞が細くなるだけで、自分自身と比べるときのずれはなくならないからです。
mapSizeを既定の512から2048に上げた画像です。同心円の数が増えて1本1本が細くなりましたが、縞そのものは残っています。解像度を上げると描画も重くなるので、シャドウアクネの対策としては選びにくい方法です。
shadow.biasで消したときに残るもの
結論:縞は消えますが、影の境目がずれて、丼の縁にギザギザが出ました。
shadow.biasは、奥行きを比べるときに少しずらして、自分自身を影と判定しにくくする設定です。負の小さな値を入れると縞は消えました。ただ、丼が自分に落とす影の境目もずれるので、右下の縁に階段状の段差が見えます。
shadow.normalBiasで消したときに残るもの
結論:縞は消えますが、丼の内側全体が影の中に入ったように暗くなりました。
shadow.normalBiasは、影を調べる位置を面の法線(面に垂直な向き)に沿ってずらす設定です。three.jsのドキュメントでは、光が浅い角度で当たる面のシャドウアクネを減らすのに役立つと説明されています。0.02で縞は消えましたが、丼の内側がほぼ全面暗くなり、元の見た目とは別物になりました。凹んだ面では、ずらした位置が丼の縁の影に入ってしまうためだと考えていますが、そこまでは確かめていません。
castShadowを切ってよいのはどんなときか
結論:その物体が、ほかの物体に影を落とす必要がないときです。影を受けるreceiveShadowはtrueのままにできます。
丼はいちばん外側の器で、丼の影を受ける物体はありません。castShadowをfalseにしても失うものが無く、receiveShadowはtrueのままなので、サイコロの影はこれまでどおり丼に落ちます。
画像はcastShadow = falseに加えて、shadow.bias = -0.0015とmapSize 1024も入れたものです。castShadow = falseだけでも縞は消えましたが、mapSizeが512のままだとサイコロの影の輪郭が少し粗く見えました。
▼シャドウアクネを消すときの選び方
▶ ほかの物体に影を落とさなくていい → その物体のcastShadowをfalseにする
▶ 影を落とす必要もある → shadow.biasかshadow.normalBiasを小さい値から試し、縁と凹んだ面の見た目を確かめる
▶ 解像度(mapSize)は影の輪郭をきれいにするために使い、縞の対策には使わない
縞を消したあとに白飛びが目立ったときは
結論:ライトを遠ざけて強さを下げ、その分を環境光で補うと、丼の底の白飛びが消えました。
縞を消して面がなめらかになると、今度は丼の底の白飛びが目立つようになりました。PointLightは距離で明るさが落ちるので、ライトにいちばん近い丼の底だけが極端に明るくなっていたからです。縞が出ているあいだは、縞に紛れて気づいていませんでした。
light1.position.set(3, 11, 9); // (2, 6, 6) から遠ざける
light1.intensity = 0.75; // 1.0 から下げる
light2.position.set(-3, 7, -6); // (-2, 4, -4) から遠ざける
light2.intensity = 0.55; // 0.8 から下げる
ambientLight.intensity = 0.85; // 0.6 から上げて、全体の暗さを補う
material.roughness = 0.62; // 0.4 から上げ、ハイライトを広く弱く
material.metalness = 0.06; // 0.2 から下げる
ライトの向き(右上手前)は変えていないので、陰影の付き方は保たれ、サイコロの影も同じ位置に落ちます。
技術的に正しくても元に戻した理由
結論:縞と白飛びは消えましたが、丼の見た目が元の雰囲気と別物になったので、アプリでは元の設定に戻しました。
同じ丼を使っているiOSアプリ版で、ここまでの変更を一度入れました。縞も白飛びも消えて、描画としては正しくなりました。ところが元の画面と並べて見ると、丼の見た目が元とは別物になっていて、ゲームの第一印象が変わってしまいました。
描画の正しさと、見た人が感じるゲームらしさは別の軸です。どちらを優先するかは作品ごとの判断で、このときは元の見た目に戻すほうを選びました。影の設定を変えるときは、縞が消えたかだけでなく、全体の印象が変わっていないかも並べて確かめるようにしています。
関連する記事
▶ ブラウザで遊べる3Dチンチロ、Three.js&Ammo.jsで作ったサイコロゲーム…この丼を使っているゲームの作り方
▶ リペリングエフェクトの仕組み 基本・応用実装例…Three.jsのパーティクルと組み合わせる例
まとめ
▼要点
① 丼の同心円の縞は、自分自身に落とす影の計算誤差(シャドウアクネ)
② 条件は、castShadowとreceiveShadowが両方trueでshadow.biasが0のまま
③ 解像度を上げるだけでは消えない。縞が細かくなるだけ
④ 影を落とす必要がなければcastShadow = falseがいちばんきれいに消える
⑤ biasとnormalBiasは、縁のギザギザや凹んだ面の暗さと引き換えになる
縞の原因がテクスチャではなく影だと気づけたのは、丼に何も貼っていなかったからでした。単色なのに模様が出ていたら、まずcastShadowとreceiveShadowの組み合わせを疑ってみてください。
※three.js r128(UMD版)とr186(ES Modules版)を、Chrome 152のヘッドレスモード(SwiftShader)で描画して2026年9月12日に確認しました。縞の見え方はGPUや画面の解像度によって変わります。