Three.jsで作ったサイコロゲームで、丼(どんぶり)の内側に木目のような同心円の縞がずっと出ていました。丼にはテクスチャを1枚も貼っておらず、単色のマテリアルです。

結論から書くと、原因はシャドウアクネ(物体が自分自身に落とす影の計算で出る縞)でした。丼に影を落とさせないcastShadow = falseが、いちばんきれいに縞を消せました。shadow.biasshadow.normalBiasでも縞は消えますが、縁のギザギザや内側の暗さと引き換えになります。影の解像度を上げるだけでは消えません。

この記事では、丼の設定を再現して設定ごとに描画した結果と、縞を消したあとに目立った白飛びの直し方をまとめます。

丼に同心円の縞が出る原因は何か

結論:丼が自分自身に影を落としていて、その影の計算の誤差が曲面の上で縞になっていました。これをシャドウアクネと呼びます。

three.js r128で描画した丼。内側に同心円の縞(シャドウアクネ)が出ている

ブラウザで遊べる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);

同じ物体がcastShadowreceiveShadowの両方をtrueにしていて、shadow.biasが0のままという組み合わせが、シャドウアクネの条件です。

最新のthree.jsでも縞は出るか

結論:出ます。2026年9月時点の最新のr186でも、模様は細かい横縞に変わりましたが、縞は消えませんでした。

three.js r186で描画した丼。細かい横縞のシャドウアクネが出ている

r155以降はライトの明るさの単位が変わっているので、明るさをMath.PI倍にし、PointLightの減衰(decay)を0にして、r128に近い見た目で比べました。縞の模様は変わっても、原因と直し方は同じです。

設定ごとに縞はどう変わるか

結論:丼のcastShadowfalseにするのが、見た目を変えずに縞を消せる唯一の設定でした。

r128で、設定を1つずつ変えて描画した結果です。

変えた設定 ほかの変化
shadow.mapSizeを2048に(解像度だけ上げる) 残る 縞が細かくなるだけ
shadow.bias = -0.0015mapSizeを1024に 消える 丼の右下の縁に階段状のギザギザ
shadow.normalBias = 0.02 消える 丼の内側全体が暗くなる
shadow.normalBias = 0.05 消える 内側が暗く、右の縁にギザギザ
丼のcastShadow = false 消える なし(サイコロの影は丼に落ちる)

解像度を上げても縞が消えない理由

結論:シャドウマップの解像度を上げると、1つの縞が細くなるだけで、自分自身と比べるときのずれはなくならないからです。

shadow.mapSizeを2048に上げた丼。縞が細かくなっただけで残っている

mapSizeを既定の512から2048に上げた画像です。同心円の数が増えて1本1本が細くなりましたが、縞そのものは残っています。解像度を上げると描画も重くなるので、シャドウアクネの対策としては選びにくい方法です。

shadow.biasで消したときに残るもの

結論:縞は消えますが、影の境目がずれて、丼の縁にギザギザが出ました。

shadow.biasを-0.0015にした丼。縞は消えたが右下の縁にギザギザが出ている

shadow.biasは、奥行きを比べるときに少しずらして、自分自身を影と判定しにくくする設定です。負の小さな値を入れると縞は消えました。ただ、丼が自分に落とす影の境目もずれるので、右下の縁に階段状の段差が見えます。

shadow.normalBiasで消したときに残るもの

結論:縞は消えますが、丼の内側全体が影の中に入ったように暗くなりました。

shadow.normalBiasを0.02にした丼。縞は消えたが内側全体が暗くなっている

shadow.normalBiasは、影を調べる位置を面の法線(面に垂直な向き)に沿ってずらす設定です。three.jsのドキュメントでは、光が浅い角度で当たる面のシャドウアクネを減らすのに役立つと説明されています。0.02で縞は消えましたが、丼の内側がほぼ全面暗くなり、元の見た目とは別物になりました。凹んだ面では、ずらした位置が丼の縁の影に入ってしまうためだと考えていますが、そこまでは確かめていません。

castShadowを切ってよいのはどんなときか

結論:その物体が、ほかの物体に影を落とす必要がないときです。影を受けるreceiveShadowtrueのままにできます。

丼のcastShadowをfalseにした画像。縞が消え、サイコロの影は丼に落ちている

丼はいちばん外側の器で、丼の影を受ける物体はありません。castShadowfalseにしても失うものが無く、receiveShadowtrueのままなので、サイコロの影はこれまでどおり丼に落ちます。

画像はcastShadow = falseに加えて、shadow.bias = -0.0015mapSize 1024も入れたものです。castShadow = falseだけでも縞は消えましたが、mapSizeが512のままだとサイコロの影の輪郭が少し粗く見えました。

▼シャドウアクネを消すときの選び方

ほかの物体に影を落とさなくていい → その物体のcastShadowfalseにする

影を落とす必要もあるshadow.biasshadow.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が両方trueshadow.biasが0のまま

解像度を上げるだけでは消えない。縞が細かくなるだけ

④ 影を落とす必要がなければcastShadow = falseがいちばんきれいに消える

⑤ biasとnormalBiasは、縁のギザギザや凹んだ面の暗さと引き換えになる

縞の原因がテクスチャではなく影だと気づけたのは、丼に何も貼っていなかったからでした。単色なのに模様が出ていたら、まずcastShadowreceiveShadowの組み合わせを疑ってみてください。

※three.js r128(UMD版)とr186(ES Modules版)を、Chrome 152のヘッドレスモード(SwiftShader)で描画して2026年9月12日に確認しました。縞の見え方はGPUや画面の解像度によって変わります。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。