1. はじめに
今回は、Skyboxの調整と海・雲海の追加によって、ゲーム画面の遠景を整えます。
これまでゲームの仕組みを中心に実装してきましたが、画面の大半を占める背景はUnityのデフォルトのSkyboxのままで、少し寂しい印象でした。空中に浮かぶコースを走るゲームなので、眼下に広がる景色も作りたいところです。
まずは、今回の変更前後の画面をご覧ください。
| Before | After |
|---|---|
![]() |
![]() |
今回は、次の3点を実装します。
- Skyboxの調整:空の色や明るさを調整する。
- 雲海の追加:雲のテクスチャを貼ったPlaneを配置し、スクロールさせる。
- 海の追加:カメラからの距離に応じて色が変わるPlaneで、遠くに広がる海を表現する。
背景に空気感を加えるため、UnityのFogも試しましたが、今回はイメージに合わなかったため、雲海を作ることにしました。また、海と雲海はプレイヤーの移動に追従させ、走り続けても背景が途切れないようにします。
SkyboxとPlaneを組み合わせたシンプルな構成で、空の上を走る雰囲気を作っていきます。
2. Skyboxの外観を調整する
まずは、背景に使用するSkyboxのマテリアルを作成します。
今回は、空のテクスチャを新しく用意するのではなく、Skybox/Proceduralを使用し、パラメータで空の色や明るさを調整しました。
マテリアルの作成
Projectウィンドウでマテリアルを新規作成し、名前をSkyBoxとします。
ShaderにはSkybox/Proceduralを指定します。
今回使用した設定は以下のとおりです。
| 項目 | 設定値 |
|---|---|
| Sky Tint | グレー(RGB:128, 128, 128) |
| Ground | 青灰色(RGB:95, 115, 132) |
空の印象を確認しながら、主にAtmosphere Thickness、Ground、Exposureを調整します。上記の値は今回の設定例なので、作りたい雰囲気に合わせて変更してください。
シーンへの適用
LightingウィンドウのEnvironmentにあるSkybox Materialへ、作成したSkyBoxマテリアルを設定します。
今回の調整による空そのものの変化は控えめですが、続いて追加する雲海や海と組み合わせて、背景全体の印象を整えていきます。
3. 雲海を作る
続いて、コースの下に雲海を作ります。
今回は、透明度で雲の模様を表現したPNGをPlaneに貼り付け、テクスチャをスクロールさせる方法を使用します。
雲のテクスチャを用意する
Photoshopで白い雲の模様を描き、背景を透過したPNGとして書き出しました。画像サイズは1024×1024です。
RGBは白色で、アルファ値の濃淡によって雲の濃い部分と薄い部分を表現しています。
Unityに画像を取り込み、繰り返し表示とスクロールに使用するため、テクスチャのWrap ModeをRepeatに設定します。
なお、私は普段Photoshopを使わない人間なので作成したテクスチャにはあまり自信がありませんでしたが、Planeに貼り付けると意外とそれっぽく見えました。
雲海用のマテリアルを作成する
雲海用のマテリアルCloudSeaUpperを作成します。
ShaderにはUniversal Render Pipeline/Unlitを指定し、以下のように設定します。
- Base Map:用意した雲のテクスチャ
-
Tiling:X・Yともに
6 -
Base Mapの色:白、アルファ値は約
0.157(0〜1)
今回はライトによる陰影を付けず、テクスチャの色と透明度で雲を表現します。アルファ値を低めにすることで、下に配置する海が透けて見える薄い雲海にします。
Planeを配置する
雲海用のPlaneを作成し、名前をCloudSeaUpperPlaneとします。作成したCloudSeaUpperマテリアルを割り当て、Transformを以下のように設定します。
背景用のPlaneなので、Colliderは不要です。
なお、オブジェクト名にUpperが付いているのは、制作時に雲海を2層にする構成を試した名残です。今回は下層を非アクティブにし、上層の1枚だけを使用しています。
雲をスクロールさせる
雲海の制御用にCloudSeaControllerクラスを作成します。このクラスには、次の2つの役割を持たせます。
- テクスチャをスクロールさせ、雲が流れるように見せる。
- 雲海のPlaneをプレイヤーのX・Z座標に追従させる。
using UnityEngine;
namespace Resonance.Runner.Environment
{
public class CloudSeaController : MonoBehaviour
{
[SerializeField] private Renderer _targetRenderer;
[SerializeField] private Vector2 _scrollSpeed;
private Transform _followTarget;
private Vector2 _offset;
private Material _material;
private void Awake()
{
_material = _targetRenderer.material;
}
private void LateUpdate()
{
Scroll();
Follow();
}
public void SetFollowTarget(Transform followTarget)
{
_followTarget = followTarget;
}
private void Scroll()
{
_offset += _scrollSpeed * Time.deltaTime;
_material.mainTextureOffset = _offset;
}
private void Follow()
{
if (_followTarget == null)
{
return;
}
Vector3 position = transform.position;
position.x = _followTarget.position.x;
position.z = _followTarget.position.z;
transform.position = position;
}
// Renderer.materialで生成されたマテリアルを破棄する
private void OnDestroy()
{
if (_material != null)
{
Destroy(_material);
}
}
}
}
Awake()で操作対象のマテリアルを取得し、Scroll()でテクスチャのオフセットを更新します。
毎フレーム、_scrollSpeedにTime.deltaTimeを掛けた値を加算することで、フレームレートに依存しない速度で模様を動かします。Plane自体の移動とは別に、表面のテクスチャをずらすことで雲の流れを表現しています。
CloudSeaUpperPlaneにこのコンポーネントを追加し、以下のように設定します。
-
Target Renderer:
CloudSeaUpperPlaneのMesh Renderer -
Scroll Speed:
(0, -0.08)
Scroll SpeedのX・Yは、ワールド座標ではなくテクスチャのU・V方向に対応します。今回はV方向のオフセットを毎秒0.08ずつ減らしています。
また、Follow()では、SetFollowTarget()で指定されたプレイヤーのX・Z座標にPlaneを追従させます。Y座標は変更しないため、プレイヤーがジャンプしたり落下したりしても、雲海の高さは維持されます。
追跡対象を設定するGameManager側の処理は、次の節で追加します。
プレイヤーに追従させる
雲海のPlaneを固定したままでは、プレイヤーが走り続けると、いずれPlaneの範囲から外れてしまいます。
そこで、GameManagerからCloudSeaControllerにプレイヤーのTransformを渡し、雲海を追従させます。
まず、GameManagerに参照を追加します。
[SerializeField] private CloudSeaController _cloudSeaController;
次に、OnEnter()でプレイヤーを生成した直後に、追跡対象を設定します。
// プレイヤーを生成
characterManager.Spawn(new Vector3(0f, 0.08f, 0f));
// 生成したプレイヤーのTransformを取得
Transform playerCharacterTransform =
characterManager.mainCharacterGameObject.transform;
// 雲海の追跡対象に設定
_cloudSeaController.SetFollowTarget(playerCharacterTransform);
Inspectorでは、GameManagerのCloud Sea Controller欄に、CloudSeaUpperPlaneに追加したCloudSeaControllerを割り当てます。
これで、プレイヤーの移動に合わせて雲海のPlaneもX・Z方向に移動するようになります。ゲームを開始するたびに、生成されたプレイヤーを追跡対象として設定し直します。
なお、追従させてもPlaneが小さすぎると端が見えてしまいます。移動中や方向転換時のゲーム画面を確認し、端が映らない大きさに調整しておきます。
4. 距離に応じて色が変わる海を作る
雲海の下に、海を表現するPlaneを配置します。
今回は波や反射などは実装せず、カメラからの距離に応じて色を変えることで、遠くに広がる海を表現します。近くは暗い青、遠くは明るい青灰色にし、遠方の海を背景に馴染ませます。
海用のシェーダーを作成する
このシェーダーの実装にはChatGPTを利用しました。カメラからの距離に応じて、指定した2色を補間するシンプルな処理です。
SeaGradient.shaderを作成し、以下のコードを記述します。
Shader "Resonance/Environment/SeaDistanceGradient"
{
Properties
{
_NearColor ("Near Color", Color) = (0.15, 0.35, 0.55, 1)
_FarColor ("Far Color", Color) = (0.65, 0.80, 0.85, 1)
_FadeDistance ("Fade Distance", Float) = 500
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Opaque"
"Queue" = "Geometry"
}
Pass
{
Name "ForwardUnlit"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _NearColor;
float4 _FarColor;
float _FadeDistance;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
};
Varyings vert(Attributes input)
{
Varyings output;
float3 positionWS =
TransformObjectToWorld(input.positionOS.xyz);
output.positionWS = positionWS;
output.positionHCS = TransformWorldToHClip(positionWS);
return output;
}
half4 frag(Varyings input) : SV_Target
{
float2 cameraXZ = _WorldSpaceCameraPos.xz;
float2 worldXZ = input.positionWS.xz;
float dist = distance(cameraXZ, worldXZ);
float t = saturate(dist / max(_FadeDistance, 0.0001));
float4 color = lerp(_NearColor, _FarColor, t);
return color;
}
ENDHLSL
}
}
}
頂点シェーダーのvert()では、頂点の位置を描画用の座標に変換し、距離計算に使用するワールド座標をフラグメントシェーダーへ渡します。
色を決めるのは、フラグメントシェーダーのfrag()です。
float dist = distance(cameraXZ, worldXZ);
float t = saturate(dist / max(_FadeDistance, 0.0001));
float4 color = lerp(_NearColor, _FarColor, t);
まず、カメラと描画対象の位置について、高さを除いたXZ平面上の距離を求めます。
その距離を_FadeDistanceで割り、saturate()で0〜1に収めます。なお、分母のmax()はゼロ除算を避けるための処理です。
最後に、lerp()で近距離色と遠距離色を補間します。距離が0なら近距離色、_FadeDistanceの半分なら中間色、_FadeDistance以上なら遠距離色になります。
マテリアルとPlaneを作成する
Seaマテリアルを作成し、ShaderにResonance/Environment/SeaDistanceGradientを指定します。
今回は次の値を設定しました。
- Near Color:RGB(47, 79, 103)、アルファ1
- Far Color:RGB(159, 187, 192)、アルファ1
海用のPlaneを作成し、Seaマテリアルを割り当てます。雲海より下に配置し、カメラから端が見えないように大きさを調整します。背景用なのでColliderは削除します。
雲海の透明な部分から海が見えることで、単色の背景に雲の模様が重なります。
海をプレイヤーに追従させる
海も雲海と同様に、プレイヤーのX・Z座標に追従させます。海にはテクスチャのスクロールが不要なので、位置の追従だけを行うSeaControllerを作成します。
using UnityEngine;
namespace Resonance.Runner.Environment
{
public class SeaController : MonoBehaviour
{
private Transform _followTarget;
private void LateUpdate()
{
Follow();
}
public void SetFollowTarget(Transform followTarget)
{
_followTarget = followTarget;
}
private void Follow()
{
if (_followTarget == null)
{
return;
}
Vector3 position = transform.position;
position.x = _followTarget.position.x;
position.z = _followTarget.position.z;
transform.position = position;
}
}
}
海のPlaneにSeaControllerを追加し、GameManagerにも参照を追加します。
[SerializeField] private SeaController _seaController;
OnEnter()では、先ほど追加した雲海の追跡対象の設定に続けて、海にも同じTransformを渡します。
_cloudSeaController.SetFollowTarget(playerCharacterTransform);
_seaController.SetFollowTarget(playerCharacterTransform);
最後に、InspectorでGameManagerのSea Controller欄に海のPlaneのSeaControllerを割り当てます。
これで、雲海と海がそれぞれの高さを維持しながらプレイヤーに追従し、走行中も背景が広がり続けるようになります。
5. おわりに
今回は、Skyboxの調整と雲海・海の追加によって、ゲーム画面の遠景を整えました。
雲海は透明なテクスチャを貼ったPlaneをスクロールさせ、海はカメラからの距離に応じて色を変えるシェーダーで表現しています。また、どちらもプレイヤーに追従させることで、走り続けても背景が途切れないようにしました。
空そのものの変化は控えめですが、眼下に海と雲海が広がることで、Unityのデフォルトの背景が持つ印象はかなり薄れたと思います。空中に浮かぶコースを走る雰囲気も出てきました。
HLSLの基本的な用語は理解しているものの、自分で書く機会は少なかったため、今回は方針をChatGPTに伝えてシェーダーの実装を依頼しました。生成されたコードの処理内容と表示結果を確認しながら、こうした支援も取り入れて制作を進めていきたいと思います。
シンプルなPlaneとマテリアルの組み合わせでも、画面の大半を占める背景を整えると、ゲーム全体の印象が変わります。今回の実装が、背景作りの参考になれば幸いです。
この記事時点でのプレイ動画はこちらです。
🎮 Webブラウザですぐに遊べます!
本記事で制作しているゲームは itch.io にて公開中です。
インストール不要で、PCのブラウザからその場ですぐにプレイできます。
👉 ブラウザで実際に遊んでみる(itch.io)
📘 このゲームの作り方をPDFで読みたい方へ
本連載をもとに、コードや説明を見直して再編集したPDF教材を販売しています。
教材は順次執筆・更新中です。現在の収録範囲はBOOTHの商品ページをご確認ください。
👉 教材の内容を見る(BOOTH)








