Siv3D Advent Calendar 2016 1 日目の記事です。
Siv3D August 2016 v2 で使える 3D プログラムのテクニックを 3 つ紹介します。
#1. スカイスフィア
ゲームワールドでリアリティのある空を表現するには、Equirectangular 形式の 360° 天球画像を Sphere に貼り付けて描画するのが簡単です。
(▲ Equirectangular 形式の天球画像の例。配布元: http://freepanorama.blogspot.com/ )
このような画像は RICOH THETA で撮影して自作することもできますし、フリーの画像も見つかります。
スカイスフィアはシーンのライトの影響を受けないように、個別に光源を設定できる Forward Rendering で描画します。プログラムは次のとおりです。
# include <Siv3D.hpp>
void Main()
{
Window::Resize(1280, 720);
// 空と地面の境界をぼかすためにフォグを設定(空の色を使うと良い)
Graphics3D::SetFog(Fog::SquaredExponential(ColorF(0.97), 0.005));
Graphics3D::SetAmbientLight(ColorF(0.77));
// スカイスフィア用の球状の Mesh
// 裏側から球を覗くので、カリングされないように flipNormals を true にする。
const Mesh skyMesh(MeshData::Sphere(480, 24, true));
// スカイスフィア用の Equirectangular 天球画像
const Texture skyTexture(L"sky.jpg", TextureDesc::For3D);
// 地面の Mesh と Texture
const Texture terrainTexture(L"Example/Ground.jpg", TextureDesc::For3D);
const Mesh terrainMesh(MeshData::Plane(1000, { 1600, 1600 }));
while (System::Update())
{
Graphics3D::FreeCamera(0.02);
terrainMesh.draw(terrainTexture);
// 空の画像をオリジナルの色で表示するために光源をオフ。環境光を 100% に
Graphics3D::SetLightForward(0, Light::None());
Graphics3D::SetAmbientLightForward(ColorF(1.0));
{
// 空の端に到達しないよう、常にスフィアの中心にいるようにする
const Vec3 pos = Graphics3D::GetCamera().pos;
// スカイスフィアを描く
skyMesh.translated(pos.x, 0, pos.z).drawForward(skyTexture);
}
Graphics3D::SetLightForward(0, Light::Default());
Graphics3D::SetAmbientLightForward(ColorF(0.77));
}
}
#2. マウスカーソルで選択している 3D オブジェクトを調べる
3D 空間で物体を選択するするには、マウスの Ray と Box, Sphere, Plane, Disc, Triangle3D 等との交差判定を利用します。交差する物体が複数存在する場合は、最も交点が近いオブジェクトを選択します。
# include <Siv3D.hpp>
void Main()
{
Window::Resize(1280, 720);
Graphics::SetBackground(ColorF(0.4, 0.5, 0.6));
Graphics3D::SetAmbientLight(ColorF(0.8));
Graphics3D::SetAmbientLightForward(ColorF(1.0));
Graphics3D::SetLightForward(0, Light::None());
Array<Sphere> spheres;
for (size_t i = 0; i < 80; ++i)
{
spheres.push_back(Sphere(RandomVec3({ -8, 8 }, { 0.5, 0.5 }, { -8, 8 }), 0.5));
}
while (System::Update())
{
Graphics3D::FreeCamera();
Optional<size_t> selected;
double nearestDistance = Largest<double>();
const Ray mouseRay = Mouse::Ray();
const Vec3 cameraPos = Graphics3D::GetCamera().pos;
for (size_t i = 0; i < spheres.size(); ++i)
{
if (const auto pos = mouseRay.intersectsAt(spheres[i]))
{
const double d = pos->distanceFromSq(cameraPos);
if (d < nearestDistance)
{
selected = i;
nearestDistance = d;
}
}
}
Plane(24).draw();
for (size_t i = 0; i < spheres.size(); ++i)
{
if (i == selected)
{
spheres[i].draw(Palette::Red).drawShadow();
}
else
{
spheres[i].draw(HSV(i * 3, 0.4, 0.8)).drawShadow();
}
}
Cursor::SetStyle(selected ? CursorStyle::Hand : CursorStyle::Default);
}
}
#3. プログラムで作った 3D 形状を Mesh として描画する
複雑な 3D 形状をプログラムで作成する場合や、.obj 形式以外のモデルファイルを自前で読み込む場合は、頂点とインデックスの配列を MeshData に用意することで、Mesh として描画できます。
頂点配列 Array<MeshVertex>
は各頂点の 座標 (Float3) と テクスチャ UV (Float2) と 法線 (Float3) の配列で構成されます。
インデックス配列 Array<uint32>
は、頂点配列の「何番目」と「何番目」と「何番目」の頂点で三角形を構成するかを表す配列で、サイズは必ず 3 の倍数になります。
法線を計算するのが面倒な場合には、自動で計算してくれる Mesh::computeNormals()
を使います。
# include <Siv3D.hpp>
MeshData CreateMeshData()
{
const int32 N = 400;
Array<MeshVertex> vertices;
for (int32 i : step(N))
{
MeshVertex v;
v.texcoord.set(0, 0);
v.normal.set(0, 1, 0);
v.position = Vec3(Cylindrical(i * 0.01, i * 0.05, i * 0.01));
vertices.push_back(v);
v.position = Vec3(Cylindrical(i * 0.01, i * 0.05, 0.4 + i * 0.01));
vertices.push_back(v);
}
// 裏面用の頂点
for (int32 i : step(N))
{
vertices.push_back(vertices[i * 2 + 0]);
vertices.push_back(vertices[i * 2 + 1]);
}
Array<uint32> indices;
for (int32 i : step(N - 1))
{
indices.push_back(2 * i + 0);
indices.push_back(2 * i + 2);
indices.push_back(2 * i + 1);
indices.push_back(2 * i + 1);
indices.push_back(2 * i + 2);
indices.push_back(2 * i + 3);
}
const uint32 offset = N * 2;
// 裏面用のインデックス
for (int32 i : step(N - 1))
{
indices.push_back(offset + 2 * i + 0);
indices.push_back(offset + 2 * i + 1);
indices.push_back(offset + 2 * i + 2);
indices.push_back(offset + 2 * i + 1);
indices.push_back(offset + 2 * i + 3);
indices.push_back(offset + 2 * i + 2);
}
MeshData meshData(vertices, indices);
meshData.computeNormals();
return meshData;
}
void Main()
{
Window::Resize(1280, 720);
Graphics::SetBackground(ColorF(0.5, 0.7, 0.9));
Graphics3D::SetAmbientLight(ColorF(0.4));
const Texture textureTerrain(L"Example/Grass.jpg", TextureDesc::For3D);
const Mesh mesh(CreateMeshData());
while (System::Update())
{
Graphics3D::FreeCamera();
Plane(20).draw(textureTerrain);
mesh.translated(0, 0, -4).draw().drawShadow();
}
}
明日の Siv3D Advent Calendar の記事は @Mitsugoro32 さんです。
よろしくお願いします。