🧱

Siv3D | Polygon から 3D の壁を作る

14 min read

これは Siv3D Advent Calendar 2021 の参加記事です。

画像のように、Polygon から 3D の壁の Mesh を作成するコードを紹介します。実装した関数は

MeshData CreateWall(const Polygon& polygon, double h);

だけです。

これとは別に、壁の接地感を高めるために、

  • 疑似的な Ambient Occlusion を床のテクスチャに事前に描き込んでおく
  • 高さと法線ベースの非常に単純な Ambient Occlusion を triplanar ピクセルシェーダに追加する

というテクニックを導入しています。その ON/OFF をチェックボックスから切り替えられます。

# include <Siv3D.hpp> // OpenSiv3D v0.6.3

MeshData CreateWall(const Polygon& polygon, double h)
{
	if (not polygon)
	{
		return{};
	}

	const float hF = static_cast<float>(h);

	// Face Vertex
	const uint32 faceVertexSize = static_cast<uint32>(polygon.vertices().size());
	Array<Vertex3D> vertices(faceVertexSize * 2);
	{
		uint32 index = 0;

		for (size_t i = 0; i < faceVertexSize; ++i)
		{
			const Float2 pos = polygon.vertices()[index];

			auto& vertexUp = vertices[i];
			auto& vertexBottom = vertices[i + faceVertexSize];

			vertexUp.pos.set(pos.x, hF, -pos.y);
			vertexUp.normal.set(0.0f, 1.0f, 0.0f);
			vertexUp.tex.set(0.0f, 0.0f);

			vertexBottom.pos.set(pos.x, 0.0f, -pos.y);
			vertexBottom.normal.set(0.0f, -1.0f, 0.0f);
			vertexBottom.tex.set(0.0f, 0.0f);

			++index;
		}
	}

	// Face Index
	const size_t faceIndexSize = polygon.indices().size();
	Array<TriangleIndex32> indices(faceIndexSize * 2);
	{
		const auto& polygonIndices = polygon.indices();

		for (size_t i = 0; i < faceIndexSize; ++i)
		{
			const auto& t = polygonIndices[i];
			indices[i] = { t.i0, t.i1, t.i2 };
		}

		for (size_t i = 0; i < faceIndexSize; ++i)
		{
			const auto& t = polygonIndices[i];
			indices[faceIndexSize * 2 - i - 1] = { (t.i0 + faceVertexSize), (t.i2 + faceVertexSize), (t.i1 + faceVertexSize) };
		}
	}

	// Outer Vertex
	const size_t outerVertexSizeOne = polygon.outer().size();
	const size_t outerVertexSize = (outerVertexSizeOne * 4);
	Array<Float3> outerPositions(outerVertexSize);
	{
		for (size_t i = 0; i < outerVertexSizeOne; ++i)
		{
			const Float2 posA = polygon.outer()[i];
			const Float2 posB = polygon.outer()[(i + 1) % outerVertexSizeOne];
			outerPositions[i * 4 + 0].set(posA.x, hF, -posA.y);
			outerPositions[i * 4 + 1].set(posA.x, 0.0f, -posA.y);
			outerPositions[i * 4 + 2].set(posB.x, hF, -posB.y);
			outerPositions[i * 4 + 3].set(posB.x, 0.0f, -posB.y);
		}
	}

	// Outer Index
	const size_t outerIndexSize = outerVertexSizeOne * 2;
	Array<TriangleIndex32> outerIndices(outerIndexSize);
	{
		for (uint32 i = 0; i < outerVertexSizeOne; ++i)
		{
			outerIndices[i * 2 + 0] =
			{
				(i * 4 + 0),
				(i * 4 + 1),
				(i * 4 + 3)
			};

			outerIndices[i * 2 + 1] =
			{
				(i * 4 + 3),
				(i * 4 + 2),
				(i * 4 + 0)
			};
		}
	}

	// Holes Vertex & Index
	uint32 holeIndexOffsetBase = static_cast<uint32>(outerVertexSize);

	for (const auto& hole : polygon.inners())
	{
		const uint32 holeVertexSizeOne = static_cast<uint32>(hole.size());
		const uint32 holeVertexSize = static_cast<uint32>(hole.size()) * 4;
		Array<Float3> holePositions(holeVertexSize);

		for (size_t i = 0; i < holeVertexSizeOne; ++i)
		{
			const Float2 posA = hole[i];
			const Float2 posB = hole[(i + 1) % holeVertexSizeOne];
			holePositions[i * 4 + 0].set(posA.x, hF, -posA.y);
			holePositions[i * 4 + 1].set(posA.x, 0.0f, -posA.y);
			holePositions[i * 4 + 2].set(posB.x, hF, -posB.y);
			holePositions[i * 4 + 3].set(posB.x, 0.0f, -posB.y);
		}

		outerPositions.insert(outerPositions.end(), holePositions.begin(), holePositions.end());

		const size_t holeIndexSize = holeVertexSizeOne * 2;
		Array<TriangleIndex32> holeIndices(holeIndexSize);

		for (uint32 i = 0; i < holeVertexSizeOne; ++i)
		{
			holeIndices[i * 2 + 0] =
			{
				(holeIndexOffsetBase + i * 4 + 0),
				(holeIndexOffsetBase + i * 4 + 1),
				(holeIndexOffsetBase + i * 4 + 3)
			};

			holeIndices[i * 2 + 1] =
			{
				(holeIndexOffsetBase + i * 4 + 3),
				(holeIndexOffsetBase + i * 4 + 2),
				(holeIndexOffsetBase + i * 4 + 0)
			};
		}

		holeIndexOffsetBase += holeVertexSize;
		outerIndices.insert(outerIndices.end(), holeIndices.begin(), holeIndices.end());
	}

	{
		const uint32 outerIndexOffset = faceVertexSize * 2;

		for (auto& outerIndex : outerIndices)
		{
			outerIndex.i0 += outerIndexOffset;
			outerIndex.i1 += outerIndexOffset;
			outerIndex.i2 += outerIndexOffset;
		}
	}

	// Union
	{
		const size_t outerVertxSizeAll = outerPositions.size();
		vertices.resize(vertices.size() + outerVertxSizeAll);

		for (size_t i = 0; i < outerVertxSizeAll; ++i)
		{
			auto& vertex = vertices[faceVertexSize * 2 + i];
			vertex.pos = outerPositions[i];
		}

		const size_t outerIndexSizeAll = outerIndices.size();
		indices.append(outerIndices);
	}

	MeshData meshData{ std::move(vertices), std::move(indices) };
	meshData.computeNormals();
	return meshData;
}

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	Image uvCheckerImage{ U"example/texture/uv.png" };
	const Texture uvChecker{ uvCheckerImage, TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	// triplanar 用のシェーダ
	const PixelShader ps = HLSL{ U"example/shader/hlsl/forward_triplanar.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/forward_triplanar.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }} };

	// 疑似的な Ambient Occlusion を追加した triplanar 用のシェーダ
	const PixelShader psAO = HLSL{ U"forward_triplanar2.hlsl", U"PS" }
		| GLSL{ U"forward_triplanar2.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }} };

	if ((not ps) || (not psAO))
	{
		return;
	}

	// 木材のテクスチャ
	const Texture woodTexture{ U"example/texture/wood.jpg", TextureDesc::MippedSRGB };

	// 壁の Polygon と高さ
	const Array<std::pair<Polygon, double>> polygons =
	{
		{ Shape2D::Arrow(Vec2{-24, -16}, Vec2{-12, -16}, 1, Vec2{ 4, 4 }), 0.5 },
		{ Shape2D::Hexagon(6.0, Vec2{0, -16}), 4.0 },
		{ Shape2D::Cross(6.0, 3.0, Vec2{16, -16}), 2.0 },
		{ Shape2D::Stairs(Vec2{ -8, 0 }, 8, 8, 8), 2.0 },
		{ Shape2D::Star(8).asPolygon().addHole(Circle{ 2 }.asPolygon().outer().reversed()) , 0.5 },
		{ Rect{ Arg::center(16, 0), 8 }.asPolygon().addHole(Rect{ Arg::center(16, 0), 6 }.asPolygon().outer().reversed()), 2.0 }
	};

	// 疑似的な Ambient Occlusion を床テクスチャに描き込み
	{
		Image image{ 2048, 2048, Color{255} };
		{
			for (const auto& polygon : polygons)
			{
				polygon.first.movedBy(32, 32).scale(2048 / 64.0).overwrite(image, ColorF{ 0.5 });
			}

			image.gaussianBlur(17);
		}

		for (auto p : step(image.size()))
		{
			Color& color = uvCheckerImage[p];
			color = (ColorF{ color } * ColorF{ image[p] });
		}
	}
	const Texture uvCheckerAO{ uvCheckerImage, TextureDesc::MippedSRGB };

	// 壁のメッシュ
	const Array<Mesh> walls = polygons.map([](const std::pair<Polygon, double>& shape)
	{
		return Mesh{ CreateWall(shape.first, shape.second) };
	});

	// 疑似的な Ambient Occlusion
	bool simpleAO = true;

	while (System::Update())
	{
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// 3D 描画
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			if (simpleAO)
			{
				Plane{ 64 }.draw(uvCheckerAO);

				const ScopedCustomShader3D shader{ psAO };

				for (const auto& wall : walls)
				{
					wall.draw(woodTexture);
				}
			}
			else
			{
				Plane{ 64 }.draw(uvChecker);

				const ScopedCustomShader3D shader{ ps };

				for (const auto& wall : walls)
				{
					wall.draw(woodTexture);
				}
			}
		}

		// 3D シーンを 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		SimpleGUI::CheckBox(simpleAO, U"Simple AO", Vec2{ 20,20 });
	}
}

ピクセルシェーダはそれぞれ // * で注釈をつけた 2 行のみ変更しています。

forward_triplanar2.hlsl
//-----------------------------------------------
//
//	This file is part of the Siv3D Engine.
//
//	Copyright (c) 2008-2021 Ryo Suzuki
//	Copyright (c) 2016-2021 OpenSiv3D Project
//
//	Licensed under the MIT License.
//
//-----------------------------------------------

//
//	Textures
//
Texture2D		g_texture0 : register(t0);
SamplerState	g_sampler0 : register(s0);

namespace s3d
{
	//
	//	VS Output / PS Input
	//
	struct PSInput
	{
		float4 position : SV_POSITION;
		float3 worldPosition : TEXCOORD0;
		float2 uv : TEXCOORD1;
		float3 normal : TEXCOORD2;
	};
}

//
//	Constant Buffer
//
cbuffer PSPerFrame : register(b0)
{
	float3 g_gloablAmbientColor;
	float3 g_sunColor;
	float3 g_sunDirection;
}

cbuffer PSPerView : register(b1)
{
	float3 g_eyePosition;
}

cbuffer PSPerMaterial : register(b3)
{
	float3 g_amibientColor;
	uint   g_hasTexture;
	float4 g_diffuseColor;
	float3 g_specularColor;
	float  g_shininess;
	float3 g_emissionColor;
}

//
//	Functions
//
float4 Triplanar(float3 worldPos, float3 normal, float uvScale)
{
	float4 diffuseColor = g_diffuseColor;

	if (g_hasTexture)
	{
		float3 blend = abs(normal);
		blend /= (blend.x + blend.y + blend.z);
		
		worldPos *= uvScale;
		const float4 c0 = g_texture0.Sample(g_sampler0, worldPos.yz);
		const float4 c1 = g_texture0.Sample(g_sampler0, worldPos.xz);
		const float4 c2 = g_texture0.Sample(g_sampler0, worldPos.xy);

		diffuseColor *= (c0 * blend.x + c1 * blend.y + c2 * blend.z);
	}

	return diffuseColor;
}

float3 CalculateDiffuseReflection(float3 n, float3 l, float3 lightColor, float3 diffuseColor, float3 ambientColor)
{
	const float3 directColor = lightColor * saturate(dot(n, l));
	return ((ambientColor + directColor) * diffuseColor);
}

float3 CalculateSpecularReflection(float3 n, float3 h, float shininess, float nl, float3 lightColor, float3 specularColor)
{
	const float highlight = pow(saturate(dot(n, h)), shininess) * float(0.0 < nl);
	return (lightColor * specularColor * highlight);
}

float4 PS(s3d::PSInput input) : SV_TARGET
{
	const float3 lightColor		= g_sunColor;
	const float3 lightDirection	= g_sunDirection;

	const float3 n = normalize(input.normal);
	const float3 l = lightDirection;
	const float4 diffuseColor = Triplanar(input.worldPosition, n, 0.125f);
	const float simpleAO = 0.5 + min(input.worldPosition.y + input.normal.y, 0.5); // *
	const float3 ambientColor = (g_amibientColor * g_gloablAmbientColor) * simpleAO; // *

	// Diffuse
	const float3 diffuseReflection = CalculateDiffuseReflection(n, l, lightColor, diffuseColor.rgb, ambientColor);

	// Specular
	const float3 v = normalize(g_eyePosition - input.worldPosition);
	const float3 h = normalize(v + lightDirection);
	const float3 specularReflection = CalculateSpecularReflection(n, h, g_shininess, dot(n, l), lightColor, g_specularColor);

	return float4(diffuseReflection + specularReflection + g_emissionColor, diffuseColor.a);
}
forward_triplanar2.frag
//	Copyright (c) 2008-2021 Ryo Suzuki.
//	Copyright (c) 2016-2021 OpenSiv3D Project.
//	Licensed under the MIT License.

# version 410

//
//	Textures
//
uniform sampler2D Texture0;

//
//	PSInput
//
layout(location = 0) in vec3 WorldPosition;
layout(location = 1) in vec2 UV;
layout(location = 2) in vec3 Normal;

//
//	PSOutput
//
layout(location = 0) out vec4 FragColor;

//
//	Constant Buffer
//
layout(std140) uniform PSPerFrame // slot 0
{
	vec3 g_gloablAmbientColor;
	vec3 g_sunColor;
	vec3 g_sunDirection;
};

layout(std140) uniform PSPerView // slot 1
{
	vec3 g_eyePosition;
};

layout(std140) uniform PSPerMaterial // slot 3
{
	vec3  g_amibientColor;
	uint  g_hasTexture;
	vec4  g_diffuseColor;
	vec3  g_specularColor;
	float g_shininess;
	vec3  g_emissionColor;
};

//
//	Functions
//
vec4 Triplanar(vec3 worldPos, vec3 normal, float uvScale)
{
	vec4 diffuseColor = g_diffuseColor;

	if (g_hasTexture == 1)
	{
		vec3 blend = abs(normal);
		blend /= (blend.x + blend.y + blend.z);
		
		worldPos *= uvScale;
		vec4 c0 = texture(Texture0, worldPos.yz);
		vec4 c1 = texture(Texture0, worldPos.xz);
		vec4 c2 = texture(Texture0, worldPos.xy);

		diffuseColor *= (c0 * blend.x + c1 * blend.y + c2 * blend.z);
	}

	return diffuseColor;
}

vec3 CalculateDiffuseReflection(vec3 n, vec3 l, vec3 lightColor, vec3 diffuseColor, vec3 ambientColor)
{
	vec3 directColor = lightColor * max(dot(n, l), 0.0f);
	return ((ambientColor + directColor) * diffuseColor);
}

vec3 CalculateSpecularReflection(vec3 n, vec3 h, float shininess, float nl, vec3 lightColor, vec3 specularColor)
{
	float highlight = pow(max(dot(n, h), 0.0f), shininess) * float(0.0f < nl);
	return (lightColor * specularColor * highlight);
}

void main()
{
	vec3 lightColor		= g_sunColor;
	vec3 lightDirection	= g_sunDirection;

	vec3 n = normalize(Normal);
	vec3 l = lightDirection;
	vec4 diffuseColor = Triplanar(WorldPosition, n, 0.25f);
	float simpleAO = 0.5f + min(WorldPosition.y + Normal.y, 0.5f); // *
	vec3 ambientColor = (g_amibientColor * g_gloablAmbientColor) * simpleAO; // *

	// Diffuse
	vec3 diffuseReflection = CalculateDiffuseReflection(n, l, lightColor, diffuseColor.rgb, ambientColor);

	// Specular
	vec3 v = normalize(g_eyePosition - WorldPosition);
	vec3 h = normalize(v + lightDirection);
	vec3 specularReflection = CalculateSpecularReflection(n, h, g_shininess, dot(n, l), lightColor, g_specularColor);

	FragColor = vec4(diffuseReflection + specularReflection + g_emissionColor, diffuseColor.a);
}

Discussion

ログインするとコメントできます