6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Siv3DAdvent Calendar 2021

Day 13

カメラワークに使える関数の紹介

Last updated at Posted at 2021-12-12

#Siv3Dで3Dモデルを簡単に表示したい
Siv3DはV0.6から3D表示の機能が追加されて、面白くなってきました。
3Dモデルのデータを保持するクラスと、ベクトルや行列を扱うクラスが、直感的でわかりやすく記述できるので、これならやってみようかと思う人も多いのではないでしょうか。

去年は、旧Siv3DでglTFのモデルの描画をやっていましたけど、表示がある程度できるようになると、何かもの足りない感じがしてきます。
ゲームやアニメーションだと構図が凝っていて、狙っているカメラワークが作り手の狙い通りの表現になっていると思います。
その「狙い通り」をやりたところで、角度指定では狙い通りというには難しい状況があります。
もちろんSiv3Dには、ベクトルベースの角度指定方法も用意されていてQuaternion::FromUnitVectorPairs()を使って作れるので、自分なりの3D便利関数を組み立てていく事は楽しいかもしれません。
今回の記事は、私なりの3D便利関数の紹介になります。

#カメラワーク用関数
動く被写体を捉えながら角度を変更していきたいので、視点や注視点を移動して角度変更を行う処理を色々まとめて用意します。
カメラなので、Siv3DのBasicCamera3Dをベースに機能を拡張していきます。
※今年は、開発コード:Pixieという呼称を使っていたので、クラス名はPixieCameraにしています。
また、旧Siv3DからOpenSiv3DのV0.6に合わせて、glTFやメッシュ制御を使えるようにしたクラスをPixieMeshという名称で作っていました。

カメラワーク用に用意して関数では共通で、入力値はカメラクラス内の注視点m_focusPosと、視点m_eyePosになっているので、あらかじめsetFocusPosition()やsetEyePosition()で設定しておく必要があります。
カメラワーク用関数を実行すると、引数で指定した変化量で、注視点m_focusPosと、視点m_eyePosが更新されます。

副次的にm_focusPosDeltaとm_eyePosDelta には変化量が保存されます。
こちらは、getEyePosDelta()やgetFocusPosDelta()で取得して利用できます。

この処理は、まだ検討中かつ他の処理系がどうやっているかも不勉強者なので知らないで書いてるのですが、3Dモデルは使っていくと大抵「向き」の情報が必要になります。
発展していくと視野角など必要になるかもしれません。
だったら、いっそBasicCamera3Dを内包しておけば、カメラワーク用の関数で、3Dモデルの移動もやってしまえるし、きっと便利に記述できるに違いあるまいかと。

また、RenderTexture等へカメラの映像を描画する場合にはBasicCamera3Dクラスの使い方に合わせて、setView()やsetProjection()を使って、内部情報を更新しておく必要があるでしょう。

※カメラワーク用関数はここに挙げたものが全てではないので、必要に応じて順次追加していく予定です。

###パン(PanX)
カメラの位置を固定して、カメラの角度を水平に振ります。
引数のleftrightで振幅を指定すると、m_focusPosの内容が更新されます。
panx.gif

		PixieCamera& panX(float leftright)
		{
			Float3 forward = m_focusPos - m_eyePos;
			Float3 dir = forward.normalized();
			Float3 right = dir.cross(m_upDir);
			m_upDir = right.cross(dir).normalized();
			SIMD_Float4 qrot = DirectX::XMQuaternionRotationAxis(SIMD_Float4{ m_upDir, 0 }, leftright);
			SIMD_Float4 focus = DirectX::XMVector3Rotate(SIMD_Float4{ forward, 0 }, qrot);
			Float3 oldfocus = m_focusPos;
			m_eyePosDelta = Float3{0,0,0};
			m_focusPos = m_eyePos + focus.xyz() ;
			m_focusPosDelta = m_focusPos - oldfocus;
			return *this;
		}

###チルトアップ/ダウン(Tilt/PanY)
水平のパンに対して上下にカメラを振るパンがチルトアップ/ダウンです。
引数のupdownで振幅を指定すると、m_focusPosの内容が更新されます。
pany.gif

		PixieCamera& panY(float updown)
		{
			Float3 forward = m_focusPos - m_eyePos;
			Float3 dir = forward.normalized();
			Float3 right = dir.cross(m_upDir);
			m_upDir = right.cross(forward).normalized();
			SIMD_Float4 qrot = DirectX::XMQuaternionRotationAxis(SIMD_Float4{ right, 0 }, updown);
			SIMD_Float4 focus = DirectX::XMVector3Rotate(SIMD_Float4{ forward, 0 }, qrot);
			Float3 oldfocus = m_focusPos;
			m_eyePosDelta = Float3{ 0,0,0 };
			m_focusPos = m_eyePos + focus.xyz() ;
			m_focusPosDelta = m_focusPos - oldfocus;
			return *this;
		}

		PixieCamera& tilt(float updown)
		{
			panY(updown);
			return *this;
		}

###トラック(TrackX)
カメラを左右に移動させながら撮影します。
引数のleftrightで振幅を指定すると、m_eyePosとm_focusPosの内容が更新されます。
trackx.gif

		PixieCamera& trackX(float leftright)
		{
			Float3 fwd = m_focusPos - m_eyePos;
			Float3 dir = fwd.normalized();
			Float3 right = dir.cross(m_upDir).normalized();

			dir = leftright * right;
			m_eyePosDelta = dir;
			m_focusPosDelta = dir;
			m_eyePos += dir;
			m_focusPos += dir;
			return *this;
		}

###クレーンアップ/ダウン(CraneY)
カメラを上下に移動させながら撮影します。
引数のupdownで振幅を指定すると、m_eyePosとm_focusPosの内容が更新されます。
crane.gif

		PixieCamera& craneY(float updown)
		{
			Float3 fwd = m_focusPos - m_eyePos;
			Float3 dir = fwd.normalized();
			Float3 right = dir.cross(m_upDir);
			m_upDir = right.cross(dir).normalized();

			dir = updown * m_upDir;
			m_eyePosDelta = dir;
			m_focusPosDelta = dir;
			m_eyePos += dir;
			m_focusPos += dir;
			return *this;
		}

###ドリー(Dolly)
カメラを前後に移動させながら撮影します。
引数のforwardbackで移動量を指定すると、m_eyePosとm_focusPosの内容が更新されます。
dolly.gif

		PixieCamera& dolly( float forwardback )
		{
			Float3 dir = (m_focusPos - m_eyePos).normalized() * forwardback;
			m_eyePosDelta = dir;
			m_focusPosDelta = dir;
			m_eyePos += dir;
			m_focusPos += dir;
			return *this;
		}

###アーク(ArcX/ArcY)
注視点を中心にカメラを円形に移動しながら撮影します。
引数のleftrightやupdownで移動量を指定すると、m_eyePosの内容が更新されます。

arcx.gif
arcy.gif

		PixieCamera& arcX(float leftright)
		{
			Float3 forward = m_eyePos - m_focusPos;
			Float3 dir = (-forward).normalized();
			Float3 right = m_upDir.cross(dir);
			m_upDir = dir.cross(right).normalized();

			SIMD_Float4 qrot = DirectX::XMQuaternionRotationAxis(SIMD_Float4{ m_upDir, 0 }, leftright);
			SIMD_Float4 eye = DirectX::XMVector3Rotate(SIMD_Float4{ forward, 0 }, qrot);

			Float3 oldeye = m_eyePos;
			m_eyePos = eye.xyz() + m_focusPos;
			m_eyePosDelta = m_eyePos - oldeye;
			m_focusPosDelta = Float3{ 0,0,0 };
			return *this;
		}

		PixieCamera& arcY(float updown)
		{
			Float3 forward = m_eyePos - m_focusPos;
			Float3 dir = (-forward).normalized();
			Float3 right = m_upDir.cross(dir);
			m_upDir = dir.cross(right).normalized();

			SIMD_Float4 qrot = DirectX::XMQuaternionRotationAxis(SIMD_Float4{ right, 0 }, updown);
			SIMD_Float4 eye = DirectX::XMVector3Rotate(SIMD_Float4{ forward, 0 }, qrot);
			Float3 oldeye = m_eyePos;
			m_eyePos = eye.xyz() + m_focusPos;
			m_eyePosDelta = m_eyePos - oldeye;
			m_focusPosDelta = Float3{ 0,0,0 };
			return *this;
		}

#使用例など
カメラワーク用と言っても、関数名がカメラの用語に合わせているというだけなので、3Dモデルの移動計算にも便利に使えますね。
そこで、使用例として何かデモ的なゲームを作ってみよう!と考えました。

無題.png

題して、
「巨大クリスマスツリーの周囲に張られた、雪の結晶軌道を光速のソリに乗って敵の侵略から守るんだ、Siv3Dくん!!」
の予定でしたが、敵はおろか肝心のカメラワークが未完成となってしまいました。

無題1.png
遠景はこんな感じです。モミの木めっちゃでかいです。
おそらくスカイツリークラスです。

ezgif.com-gif-maker.gif
季節限定Siv3Dくんサンタクロースバージョン。
折角のカメラワーク機能作る時間足らず、、別撮り。。。(笑)

ezgif.com-gif-maker (1).gif

トナカイ。よく見ると、額の模様がSiv3Dくんのフードと同じ。。(わからないですけどね。)

ezgif.com-gif-maker (3).gif
もはや未確認飛行物体のようなGIFになってしまいましたが。。
いちおう、デバッグカメラがマウス対応なので、

★デバッグカメラの操作方法
マウス右ボタンドラッグが、Track、Craneです。+LShiftで高速化。
RCtrl+マウスホイールがDollyで、前後移動。Logicoolの高速ホイールマウスが必須です。
、というのも厳しいのでWASDでTrack,Dollyできます。
マウス中ボタンドラッグでPanします。

#ソースコード(main.cppのみ)
このデモは、glTFの部分を除けば300行ぐらいです。
サンプルコード増えてくれば、数日で3Dのゲーム作れるんじゃないでしょうか。
まだまだ、バグなどたくさんあるようなものですが、もしも指摘いただけるとありがたく考えております。

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

# include "PixieMesh.hpp"
# include "PixieCamera.hpp"
# include "LineString3D.hpp"

constexpr ColorF        BGCOLOR = { 0.8, 0.9, 1.0, 0 };
constexpr TextureFormat TEXFORMAT = TextureFormat::R8G8B8A8_Unorm_SRGB;
constexpr Size			WINDOWSIZE = { 1280, 768 };
constexpr StringView	APATH = U"../Asset/" ;
constexpr RectF			PIPWINDOW{ 900,512,370,240 } ;
constexpr ColorF        WHITE = ColorF{ 1,1,1,USE_COLOR };
constexpr Vec2			TREECENTER{ -100, -100 };
constexpr double		TREERASIUS = 100;
constexpr double		VOLALITY = 40;
constexpr double		TONAKAISPEED = 0.00003;

struct ActorRecord
{
	ColorF Color = WHITE;
	Float3 Pos = Float3 {0,0,0} ;
	Float3 Sca = Float3 {1,1,1} ;
	Float3 rPos = Float3 {0,0,0} ;
	Float3 eRot = Float3 {0,0,0} ;
	Quaternion qRot = Quaternion::Identity() ;
	Float3 eyePos = Float3 {0,0,0} ;
	Float3 focusPos =  Float3 {0,0,0};
};

enum SELECTEDTARGET
{
	ST_SLED, ST_FONT, ST_CAMERA, ST_TREE, ST_GND,
	ST_TONAKI_A,
	ST_TONAKI_B, ST_TONAKI_C, ST_TONAKI_D,
	ST_TONAKI_E, ST_TONAKI_F, ST_TONAKI_G,
	NUMST
};

Array<ActorRecord> actorRecords;

Array<PixieMesh> pixieMeshes(NUMST);
PixieCamera cameraMain(WINDOWSIZE);

//トナカイ
void updateTonakai(Array<PixieMesh>& meshes, LineString3D& ls3, double & progressPos)
{
	static Array<Float3> prevpos(7);
	for (int32 i = 0; i < prevpos.size(); i++)
	{
		const double offset[7] = { 0, 0.0002,0.0002, 0.0004,0.0004, 0.0006,0.0006 };

		PixieMesh& mesh = meshes[ST_TONAKI_A + i];
		prevpos[i] = mesh.Pos;
		mesh.Pos = ls3.getCurvedPoint( progressPos+offset[i] );

		Float3 up{ 0,1,0 };
		Float3 right{ 0,0,0 };
		mesh.qRot = mesh.camera.getQLookAt(mesh.Pos, prevpos[i], &up, &right);
		if (i == 1 || i == 3 || i == 5) mesh.rPos = -right;
		if (i == 2 || i == 4 || i == 6) mesh.rPos = +right;
	}

	if (progressPos >= 1) progressPos = 0;
}

//ソリ
void updateSled(PixieMesh& mesh, LineString3D& ls3, double& progressPos)
{
	double progress = progressPos - 0.0004;
	if (progress < 0) progress += 1.0;

	Float3 prevpos = mesh.Pos;
	mesh.Pos = ls3.getCurvedPoint(progress);
	mesh.qRot = mesh.camera.getQLookAt(mesh.Pos, prevpos);
}

//トナカイカメラ
void updateCamera( PixieMesh& target, PixieMesh& mesh )
{
	mesh.camera.setFocusPosition(target.Pos);

	float speed = 0.01f;
	if (KeyRControl.pressed()) speed *= 5;
	if (KeyLeft.pressed())	mesh.rPos = mesh.camera.arcX(-speed).getEyePosition();
	if (KeyRight.pressed()) mesh.rPos = mesh.camera.arcX(+speed).getEyePosition();
	if (KeyUp.pressed())	mesh.rPos = mesh.camera.arcY(-speed).getEyePosition();
	if (KeyDown.pressed())	mesh.rPos = mesh.camera.arcY(+speed).getEyePosition();

	mesh.camera.updateView();
	mesh.camera.updateViewProj();
	mesh.setRotateQ(mesh.camera.getQForward());
}

void updateMainCamera(const PixieMesh& model, PixieCamera& camera)
{
	Float3 eyepos = camera.getEyePosition();
	Float3 focuspos = camera.getFocusPosition();

	Float2 delta = Cursor::DeltaF();
	Float3 vector = eyepos.xyz() - focuspos;
	Float2 point2D = Cursor::PosF();
	Float3 distance;

	float speedM = 1;
	if (KeyLControl.down() || MouseM.down())								//中ボタンドラッグ:回転
	{
		const Ray mouseRay = camera.screenToRay(Cursor::PosF());
		if (const auto depth = mouseRay.intersects(model.ob))
		{
			Float3 point3D = mouseRay.point_at(*depth);						//ポイントした3D座標を基点
			distance = point3D - eyepos;

			Float3 identity = distance.normalized();
			speedM = distance.length() / identity.length() / 100;
		}
	}

	if (KeyLControl.pressed() || MouseM.pressed())
	{
		bool rev = camera.dolly((float)Mouse::Wheel() * speedM / 5, true);	//中ボタンウィール:拡縮
		if (rev)
		{
			rev = camera.setEyePosition(eyepos).dolly((float)Mouse::Wheel() * speedM / 100, true);
			if (rev)
			{
				rev = camera.setEyePosition(eyepos).dolly((float)Mouse::Wheel() * speedM / 1000, true);
				if (rev) camera.setEyePosition(eyepos);
			}
		}
	}

	if (MouseR.pressed())							//右ボタンドラッグ:平行移動
	{
		if (KeyLShift.pressed()) speedM *= 5;		//Shift押下で5倍速
		camera.trackX(delta.x * speedM / 10);
		camera.craneY(delta.y * speedM / 10);
	}

	Mat4x4 mrot = Mat4x4::Identity();
	if (MouseM.pressed())
	{
		Float4 ve = { 0,0,0,0 };  // 視点移動量
		Float3 vf = { 0,0,0 };    // 注視点移動量
		if (KeyLShift.pressed())					// Shiftで5倍速
		{
			speedM = 5;
			ve *= speedM;
			vf *= speedM;
		}
		camera.arcX(delta.x * speedM / 100);
		camera.arcY(delta.y * speedM / 100);
		camera.setUpDirection(Float3{ 0,1,0 });
	}

	float speedK = camera.getBasisSpeed() / 100 * 5;
	if (KeyLShift.pressed()) speedK *= 10;			//Shift押下で5倍速

	if (KeyW.pressed()) camera.dolly(+speedK, true);
	if (KeyA.pressed()) camera.trackX(+speedK);
	if (KeyS.pressed()) camera.dolly(-speedK, true);
	if (KeyD.pressed()) camera.trackX(-speedK);

	if (KeyE.pressed()) camera.craneY(+speedK);
	if (KeyX.pressed()) camera.craneY(-speedK);
	if (KeyQ.pressed()) camera.tilt(+speedK / 100);
	if (KeyZ.pressed()) camera.tilt(-speedK / 100);

	camera.setUpDirection(Float3{ 0,1,0 });
}

void Main()
{
	//ウィンドウ初期化
	Window::Resize(WINDOWSIZE);
	Window::SetStyle(WindowStyle::Sizable);
	Scene::SetBackground(BGCOLOR);

	//描画レイヤ(レンダーテクスチャ)初期化
	static MSRenderTexture rtexMain = { (unsigned)WINDOWSIZE.x, (unsigned)WINDOWSIZE.y, TEXFORMAT, HasDepth::Yes };
	static MSRenderTexture rtexSub = { (unsigned)WINDOWSIZE.x, (unsigned)WINDOWSIZE.y, TEXFORMAT, HasDepth::Yes };

	//メッシュ設定
	PixieMesh& meshSled    = pixieMeshes[ST_SLED]   = PixieMesh{ APATH + U"XMas.Sled.006.glb", Float3{0, 0, 0} };
	PixieMesh& meshFont    = pixieMeshes[ST_FONT]   = PixieMesh{ APATH + U"ToD4Font.005.glb",  Float3{0, 0, 0} };
	PixieMesh& meshCamera  = pixieMeshes[ST_CAMERA] = PixieMesh{ APATH + U"Camera.glb",        Float3{-50, 50, -50} };
	PixieMesh& meshTree    = pixieMeshes[ST_TREE]   = PixieMesh{ APATH + U"XMas.Tree.glb",     Float3{-100, 0, -100} };
	PixieMesh& meshGND     = pixieMeshes[ST_GND]    = PixieMesh{ APATH + U"XMas.GND.glb",      Float3{0, 0, 0} };
	PixieMesh& meshTonakai = pixieMeshes[ST_TONAKI_A];

	for (int32 i = 0; i < 7; i++)
		pixieMeshes[ST_TONAKI_A + i] = PixieMesh{ APATH + U"XMas.Tonakai.006.glb", Float3{1, 0, 0} };

	//メッシュ初期化
	meshSled.initModel(MODELANI, WINDOWSIZE, NOTUSE_STRING, USE_MORPH, nullptr, HIDDEN_BOUNDBOX, 60, 0);
	meshFont.initModel(MODELNOA, WINDOWSIZE, USE_STRING, USE_MORPH);
	meshTree.initModel(MODELNOA, WINDOWSIZE, USE_STRING, USE_MORPH);
	meshCamera.initModel(MODELNOA, WINDOWSIZE);
	meshGND.initModel(MODELNOA, WINDOWSIZE);

	for (int32 i = 0; i < 7; i++)
		pixieMeshes[ST_TONAKI_A + i].initModel(MODELANI, WINDOWSIZE, NOTUSE_STRING, USE_MORPH, nullptr, HIDDEN_BOUNDBOX, 30, 0);

	//カメラ初期化
	Float4 eyePosMain = { 0, 1, 30.001, 0 };		//視点 XYZは座標、Wはカメラロールをオイラー角で保持
	Float3 focusPosMain = { 0,  0, 0 };
	cameraMain = PixieCamera(WINDOWSIZE, 45_deg, eyePosMain.xyz(), focusPosMain, 0.05);

	meshTonakai.camera = PixieCamera(WINDOWSIZE, 45_deg, meshTonakai.Pos, meshTonakai.Pos + Float3{ 0,1,0 }, 0.05);
	meshCamera.camera  = PixieCamera(WINDOWSIZE, 45_deg, meshCamera.Pos, meshSled.Pos + Float3{ 0,0,0 }, 0.05);

	// 軌道計算
	ActorRecord val;
	for (int32 yy = 0; yy < 4; yy++)
	{
		for (int32 r = 0; r < 36; r++)
		{
			const Vec2 pos2 = TREECENTER + Circular(TREERASIUS-20*yy, ToRadians(r * 10));
			val.Pos = Float3{ pos2.x + VOLALITY *(Random()-0.5), 30*yy + VOLALITY *Random(), pos2.y + VOLALITY * (Random() - 0.5) };
			actorRecords.emplace_back( val );
		}
	}
	for (int32 yy = 4; yy >= 0; --yy)
	{
		for (int32 r = 0; r < 36; r++)
		{
			const Vec2 pos2 = TREECENTER + Circular(TREERASIUS-20*yy, ToRadians(r * 10));
			val.Pos = Float3{ pos2.x + VOLALITY * (Random() - 0.5), 30 * yy + VOLALITY * Random(), pos2.y + VOLALITY * (Random() - 0.5) };
			actorRecords.emplace_back(val);
		}
	}

	LineString3D lineString3D;
	for (int32 i = 1; i < actorRecords.size(); i++)
		lineString3D.emplace_back( actorRecords[i].Pos + actorRecords[i].rPos );

	double progressPos = 0;	//現在位置をスタート位置に設定
	while (System::Update())
	{
		//メインレイヤ描画
		{
			const ScopedRenderTarget3D rtmain{ rtexMain.clear(BGCOLOR) };

			Graphics3D::SetCameraTransform(cameraMain.getViewProj(), cameraMain.getEyePosition());
			Graphics3D::SetGlobalAmbientColor(ColorF{ 1.0 });
			Graphics3D::SetSunColor(ColorF{ 1.0 });
			{
				const ScopedRenderStates3D rs{ SamplerState::RepeatAniso, RasterizerState::SolidCullFront };

				//制御
				updateMainCamera( meshGND, cameraMain );
				updateTonakai( pixieMeshes, lineString3D, progressPos );
				updateSled( meshSled, lineString3D, progressPos );
				updateCamera( meshSled, meshCamera );

				progressPos += TONAKAISPEED;
				if ( progressPos > 1.0 ) progressPos -= 1.0;

				//描画
				meshGND.drawMesh();
				meshTree.drawMesh();

				lineString3D.drawCatmullRom(actorRecords[0].Color);

				for (int32 i = 0; i < 7; i++) pixieMeshes[ST_TONAKI_A+i].drawAnime(0).nextFrame(0);
				meshSled.drawAnime(0).nextFrame(0);
				meshCamera.drawMesh();
			}

			Graphics3D::Flush();
			rtexMain.resolve();
			Shader::LinearToScreen(rtexMain);

			//サブレイヤ描画
			{
				const ScopedRenderTarget3D rtsub{ rtexSub.clear(BGCOLOR) };
				const ScopedRenderStates3D rs{ SamplerState::RepeatAniso, RasterizerState::SolidCullFront };

				Graphics3D::SetCameraTransform(meshCamera.camera.getViewProj(), meshCamera.camera.getEyePosition());
				Graphics3D::SetGlobalAmbientColor(ColorF{ 1.0 });
				Graphics3D::SetSunColor(ColorF{ 1.0 });

				meshGND.drawMesh();
				for (int32 i = 0; i < 7; i++) pixieMeshes[ST_TONAKI_A + i].drawAnime(0);
				meshSled.drawAnime(0);

				Graphics3D::Flush();
				rtexSub.resolve();
				Shader::LinearToScreen(rtexSub, PIPWINDOW);
			}
		}

	}
}

#参考URL
https://creatorways.com/basic-camera-movements/#index_id9

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?