端末の向きと動き
https://scrapbox.io/files/650988747ff1eb001bc690fc.png
OpenSiv3D for Web からスマートフォンやタブレット端末の向きや動き(加速度や角速度)にアクセスするサンプルです。
端末を傾けると 3D 空間のカメラが同じように傾きます。
タップしたまま指を上下に動かすとカメラが前後に、指を左右に動かすとカメラが左右に動きます。
また、指を動かさずに長押しするとカメラが原点の方角を向きます。
左上には端末の加速度が表示されます。
下のリンクから実際に試すことができます。
スマートフォンやタブレット端末からアクセスしてみてください。
参考
ソースコードに JavaScript を埋め込んでいます。
コピペする際は、貼り付け時のオートフォーマットで JavaScript が崩れることがある( a === b が a == = b と整形されるなど)ことに注意してください。
code:PermissionStatus.hpp
# pragma once
# include <Siv3D/Types.hpp>
/// @brief 許可状態
enum class PermissionStatus : s3d::uint8
{
/// @brief ユーザーの決定が分からない状態
Default,
/// @brief ユーザーが明示的な許可を与えている状態
Granted,
/// @brief ユーザーが明示的に拒否している状態
Denied,
};
code:Rotation.hpp
# pragma once
/// @brief 向きを表すクラス
struct Rotation
{
/// @brief z 軸を軸とする右ねじ方向の回転角
double alpha;
/// @brief x 軸を軸とする右ねじ方向の回転角
double beta;
/// @brief y 軸を軸とする右ねじ方向の回転角
double gamma;
};
code:DeviceMotionDetail.cpp
# include <emscripten.h>
namespace DeviceMotion::detail
{
maybe_unused
static int g_dummy = []
{
EM_ASM
(({
window.siv3dDeviceMotion = {
permission: (DeviceMotionEvent.requestPermission ? "default" : "granted"),
is_iOS: (/iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)),
event: {},
setEvent: function () {
window.addEventListener("devicemotion", event => this.event = event);
},
};
window.siv3dDeviceOrientation = {
permission: (DeviceOrientationEvent.requestPermission ? "default" : "granted"),
event: {},
setEvent: function () {
window.addEventListener("deviceorientation", event => this.event = event);
},
};
if (DeviceMotionEvent.requestPermission) {
const button = document.createElement("button");
button.style.position = "absolute";
button.style.left = "5%";
button.style.top = "calc(50% - 1em)";
button.style.width = "90%";
button.style.height = "2em";
button.style.fontSize = "2rem";
button.style.fontWeight = "bold";
button.textContent = "Get Permissions";
const request = async () => {
const permission = await DeviceMotionEvent.requestPermission();
if (permission === "granted" && siv3dDeviceMotion.permission !== "granted") {
siv3dDeviceMotion.setEvent();
siv3dDeviceOrientation.setEvent();
button.remove();
}
siv3dDeviceMotion.permission = permission;
siv3dDeviceOrientation.permission = permission;
};
button.addEventListener("click", request);
document.getElementById("app").appendChild(button);
}
else {
siv3dDeviceMotion.setEvent();
siv3dDeviceOrientation.setEvent();
}
}));
return 0;
}();
}
code:DeviceOrientation.hpp
# pragma once
# include "PermissionStatus.hpp"
# include "Rotation.hpp"
namespace DeviceOrientation
{
/// @brief 端末の向きを取得することに関しての許可の状態を返します。
/// @return 許可状態
nodiscard
PermissionStatus Permission();
/// @brief 端末の物理的な向きを返します。
/// @remark 0 ≤ alpha < 2π
/// @remark -π ≤ beta < π
/// @remark -π/2 ≤ gamma < π/2
/// @return 端末の向き
nodiscard
Rotation Orientation();
/// @brief 端末の向きが絶対的に(地球座標フレームを基準として)提供されているかを返します。
/// @return 地球座標フレームを基準としている場合 true, それ以外の場合は false
nodiscard
bool IsAbsolute();
}
code:DeviceOrientation.cpp
# include "DeviceOrientation.hpp"
# include <emscripten.h>
namespace DeviceOrientation
{
namespace detail
{
EM_JS
(
void, DeviceOrientation_RequestPermission, (),
{
if (DeviceOrientationEvent.requestPermission) {
(async () => {
const permission = await DeviceOrientationEvent.requestPermission();
if (permission === "granted") {
siv3dDeviceOrientation.setEvent();
}
siv3dDeviceOrientation.permission = permission;
})();
}
}
);
EM_JS
(
PermissionStatus, DeviceOrientation_GetPermission, (),
{
}
);
EM_JS
(
void, DeviceOrientation_GetOrientation, (double* pAlpha, double* pBeta, double* pGamma),
{
if (typeof(siv3dDeviceOrientation.event.alpha) === "number") {
const { alpha, beta, gamma } = siv3dDeviceOrientation.event;
setValue(pAlpha, alpha * (Math.PI / 180), "double");
setValue(pBeta, beta * (Math.PI / 180), "double");
setValue(pGamma, gamma * (Math.PI / 180), "double");
}
}
);
EM_JS
(
bool, DeviceOrientation_IsAbsolute, (),
{
return (siv3dDeviceOrientation.event.absolute ?? false);
}
);
}
void RequestPermission()
{
detail::DeviceOrientation_RequestPermission();
}
PermissionStatus Permission()
{
return detail::DeviceOrientation_GetPermission();
}
Rotation Orientation()
{
Rotation orientation{};
detail::DeviceOrientation_GetOrientation(&orientation.alpha, &orientation.beta, &orientation.gamma);
return orientation;
}
bool IsAbsolute()
{
return detail::DeviceOrientation_IsAbsolute();
}
}
code:DeviceMotion.hpp
# pragma once
# include <Siv3D/2DShapesFwd.hpp>
# include <Siv3D/Vector3D.hpp>
# include "PermissionStatus.hpp"
# include "Rotation.hpp"
namespace DeviceMotion
{
/// @brief 端末の動きを取得することに関しての許可の状態を返します。
/// @return 許可状態
nodiscard
PermissionStatus Permission();
/// @brief 端末の加速度を返します。
/// @remark 端末によっては提供されない可能性があります。その場合は常に Vec3{ 0, 0, 0 } を返します。
/// @return 端末の加速度 (m/s^2)
nodiscard
s3d::Vec3 Acceleration();
/// @brief 重力加速度を含む端末の加速度を返します。
/// @return 重力加速度を含む端末の加速度 (m/s^2)
nodiscard
s3d::Vec3 AccelerationIncludingGravity();
/// @brief 端末の向きの変化率を返します。
/// @remark 端末によっては提供されない可能性があります。その場合は常に Rotation{ 0, 0, 0 } を返します。
/// @return 端末の向きの変化率 (rad/s)
nodiscard
Rotation RotationRate();
/// @brief 端末からデータを取得する間隔を返します。
/// @return 端末からデータを取得する間隔 (秒)
nodiscard
double Interval();
}
code:DeviceMotion.cpp
# include "DeviceMotion.hpp"
# include <emscripten.h>
using namespace s3d;
namespace DeviceMotion
{
namespace detail
{
EM_JS
(
void, DeviceMotion_RequestPermission, (),
{
if (DeviceMotionEvent.requestPermission) {
(async () => {
const permission = await DeviceMotionEvent.requestPermission();
if (permission === "granted") {
siv3dDeviceMotion.setEvent();
}
siv3dDeviceMotion.permission = permission;
})();
}
}
);
EM_JS
(
PermissionStatus, DeviceMotion_GetPermission, (),
{
}
);
EM_JS
(
void, DeviceMotion_GetAcceleration, (double* pX, double* pY, double* pZ),
{
if (siv3dDeviceMotion.event.acceleration) {
const { x, y, z } = siv3dDeviceMotion.event.acceleration;
const k = (siv3dDeviceMotion.is_iOS ? -1 : 1);
setValue(pX, x * k, "double");
setValue(pY, y * k, "double");
setValue(pZ, z * k, "double");
}
}
);
EM_JS
(
void, DeviceMotion_GetAccelerationIncludingGravity, (double* pX, double* pY, double* pZ),
{
if (siv3dDeviceMotion.event.accelerationIncludingGravity) {
const { x, y, z } = siv3dDeviceMotion.event.accelerationIncludingGravity;
const k = (siv3dDeviceMotion.is_iOS ? -1 : 1);
setValue(pX, x * k, "double");
setValue(pY, y * k, "double");
setValue(pZ, z * k, "double");
}
}
);
EM_JS
(
void, DeviceMotion_GetRotationRate, (double* pAlpha, double* pBeta, double* pGamma),
{
if (siv3dDeviceMotion.event.rotationRate) {
const { alpha, beta, gamma } = siv3dDeviceMotion.event.rotationRate;
setValue(pAlpha, alpha * (Math.PI / 180), "double");
setValue(pBeta, beta * (Math.PI / 180), "double");
setValue(pGamma, gamma * (Math.PI / 180), "double");
}
}
);
EM_JS
(
double, DeviceMotion_GetInterval, (),
{
return (siv3dDeviceMotion.event.interval ?? 0) / 1000;
}
);
}
void RequestPermission()
{
detail::DeviceMotion_RequestPermission();
}
PermissionStatus Permission()
{
return detail::DeviceMotion_GetPermission();
}
Vec3 Acceleration()
{
Vec3 acceleration = Vec3::Zero();
detail::DeviceMotion_GetAcceleration(&acceleration.x, &acceleration.y, &acceleration.z);
return acceleration;
}
Vec3 AccelerationIncludingGravity()
{
Vec3 acceleration = Vec3::Zero();
detail::DeviceMotion_GetAccelerationIncludingGravity(&acceleration.x, &acceleration.y, &acceleration.z);
return acceleration;
}
Rotation RotationRate()
{
Rotation rotationRate{};
detail::DeviceMotion_GetAccelerationIncludingGravity(&rotationRate.alpha, &rotationRate.beta, &rotationRate.gamma);
return rotationRate;
}
double Interval()
{
return detail::DeviceMotion_GetInterval();
}
}
code:ScreenOrientationType.hpp
# pragma once
# include <Siv3D/Types.hpp>
/// @brief 文書の向き
enum class ScreenOrientationType : s3d::uint8
{
/// @brief 1つ目のポートレート(横長)モード
PortraitPrimary,
/// @brief 2つ目のポートレート(横長)モード
PortraitSecondary,
/// @brief 1つ目のランドスケープ(縦長)モード
LandscapePrimary,
/// @brief 2つ目のランドスケープ(縦長)モード
LandscapeSecondary,
};
code:ScreenOrientation.hpp
# pragma once
# include "ScreenOrientationType.hpp"
namespace ScreenOrientation
{
/// @brief 文書の向きを返します。
/// @return 文書の向き
nodiscard
ScreenOrientationType Type();
/// @brief 文書の向きの角度を返します。
nodiscard
double Angle();
}
code:ScreenOrientation.cpp
# include "ScreenOrientation.hpp"
# include <emscripten.h>
namespace ScreenOrientation
{
namespace detail
{
EM_JS
(
ScreenOrientationType, ScreenOrientation_GetType, (),
{
}
);
EM_JS
(
double, ScreenOrientation_GetAngle, (),
{
const angle = window.orientation ?? screen.orientation.angle;
return angle * (Math.PI / 180);
}
);
}
ScreenOrientationType Type()
{
return detail::ScreenOrientation_GetType();
}
double Angle()
{
return detail::ScreenOrientation_GetAngle();
}
}
code:DeviceLinkedCamera3D.hpp
# pragma once
# include <Siv3D/BasicCamera3D.hpp>
# include <Siv3D/Scene.hpp>
# include <Siv3D/Vector2D.hpp>
# include <Siv3D/Vector3D.hpp>
# include <Siv3D/Rect.hpp>
# include "Rotation.hpp"
class DeviceLinkedCamera3D : public s3d::BasicCamera3D
{
public:
DeviceLinkedCamera3D() = default;
DeviceLinkedCamera3D(const DeviceLinkedCamera3D&) = default;
explicit DeviceLinkedCamera3D(double verticalFOV = DefaultVerticalFOV, const s3d::Vec3& eyePosition = s3d::Vec3{ 0, 4, -4 }, const Rotation& offsetRotation = Rotation{ 0, 0, 0 }, double nearClip = DefaultNearClip) noexcept;
const Rotation& getOffset() const noexcept;
void setOffset(const Rotation& offset) noexcept;
void updateOrientation();
void updateTouchUI(double scale = 1.0, double speed = 1.0, const s3d::Rect& region = s3d::Scene::Rect());
void drawTouchUI(double scale = 1.0);
private:
Rotation m_offset{};
bool m_touched = false;
s3d::Vec2 m_touchUICenter{};
s3d::Vec2 m_stickPos{};
};
code:DeviceLinkedCamera3D.cpp
# include "DeviceLinkedCamera3D.hpp"
# include <Siv3D.hpp>
# include "DeviceOrientation.hpp"
# include "ScreenOrientation.hpp"
DeviceLinkedCamera3D::DeviceLinkedCamera3D(double verticalFOV, const Vec3& eyePosition, const Rotation& offsetRotation, double nearClip) noexcept
: BasicCamera3D(Scene::Size(), verticalFOV, eyePosition, Vec3{ 0, 0, 0 }, Vec3{ 0, 1, 0 }, nearClip)
, m_offset{ offsetRotation }
{
}
const Rotation& DeviceLinkedCamera3D::getOffset() const noexcept
{
return m_offset;
}
void DeviceLinkedCamera3D::setOffset(const Rotation& offset) noexcept
{
m_offset = offset;
}
void DeviceLinkedCamera3D::updateOrientation()
{
alpha += m_offset.alpha;
beta += m_offset.beta;
gamma += m_offset.gamma;
const double screenAngle = ScreenOrientation::Angle();
const auto orientation = (Quaternion::RotateY(screenAngle) * Quaternion::RollPitchYaw(-beta, -alpha, -gamma));
const Vec3 focusVector = (orientation * Float3{ 0, -1, 0 });
const Vec3 upDirection = (orientation * Float3{ 0, 0, 1 });
setSceneSize(Graphics3D::GetRenderTargetSize());
setView(m_eyePosition, (m_eyePosition + focusVector), upDirection);
}
void DeviceLinkedCamera3D::updateTouchUI(double scale, double speed, const Rect& region)
{
if (region.leftClicked())
{
m_touched = true;
m_touchUICenter = Cursor::PosF();
m_stickPos = Cursor::PosF();
}
else if (m_touched)
{
if (MouseL.up())
{
m_touched = false;
}
else if (MouseL.pressed())
{
const double scaledSpeed = (speed * 4 * Scene::DeltaTime());
const double lengthLimit = (150 * scale);
const Vec2 stickDelta = (Cursor::PosF() - m_touchUICenter).limitLength(lengthLimit);
m_stickPos = (m_touchUICenter + stickDelta);
if (not stickDelta.isZero())
{
const Vec3 focusVector = (m_focusPosition - m_eyePosition);
const double eyeDeltaLength = (stickDelta.length() / lengthLimit * scaledSpeed);
const double stickAngle = Vec2{ 0, -1 }.getAngle(stickDelta);
const Vec3 eyeDelta = (focusVector.withLength(eyeDeltaLength) * Quaternion::RotationAxis(m_upDirection, stickAngle));
setView((m_eyePosition + eyeDelta), (m_focusPosition + eyeDelta), m_upDirection);
}
}
}
}
void DeviceLinkedCamera3D::drawTouchUI(double scale)
{
if (m_touched)
{
Circle{ m_touchUICenter, (150 * scale) }.draw(ColorF{ 1, 0.3 });
Circle{ m_stickPos, (100 * scale) }.draw(ColorF{ 1, 0.8 });
}
}
code:Main.cpp
# include <Siv3D.hpp> // OpenSiv3D for Web v0.6.6r1
# include "DeviceLinkedCamera3D.hpp"
# include "DeviceOrientation.hpp"
# include "DeviceMotion.hpp"
// 風車の回転する羽根を描く関数
void DrawMillModel(const Model& model, const Mat4x4& mat)
{
const auto& materials = model.materials();
for (const auto& object : model.objects())
{
Mat4x4 m = Mat4x4::Identity();
// 風車の羽根の回転
if (object.name == U"Mill_Blades_Cube.007")
{
m *= Mat4x4::Rotate(Vec3{ 0,0,-1 }, (Scene::Time() * -120_deg), Vec3{ 0, 9.37401, 0 });
}
const Transformer3D transform{ (m * mat) };
object.draw(materials);
}
}
void Main()
{
// リサイズ可能にする
Window::SetStyle(WindowStyle::Sizable);
Scene::SetResizeMode(ResizeMode::Actual);
// フォント
const Font font{ FontMethod::MSDF, 48 };
// 背景色
const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
// 地面
const Mesh groundPlane{ MeshData::OneSidedPlane(2000, { 400, 400 }) };
const Texture groundTexture{ U"example/texture/ground.jpg", TextureDesc::MippedSRGB };
// 地球
constexpr Sphere earthSphere{ { 0, 1, 0 }, 1 };
const Texture earthTexture{ U"example/texture/earth.jpg", TextureDesc::MippedSRGB };
// モデルデータをロード
const Model blacksmithModel{ U"example/obj/blacksmith.obj" };
const Model millModel{ U"example/obj/mill.obj" };
const Model treeModel{ U"example/obj/tree.obj" };
const Model pineModel{ U"example/obj/pine.obj" };
const Model siv3dkunModel{ U"example/obj/siv3d-kun.obj" };
// モデルに付随するテクスチャをアセット管理に登録
Model::RegisterDiffuseTextures(treeModel, TextureDesc::MippedSRGB);
Model::RegisterDiffuseTextures(pineModel, TextureDesc::MippedSRGB);
Model::RegisterDiffuseTextures(siv3dkunModel, TextureDesc::MippedSRGB);
// レンダ―テクスチャ
const MSRenderTexture renderTexture{ 2000, 2000, TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
// カメラ
DeviceLinkedCamera3D camera{ 40_deg, Vec3{ 0, 3, -16 } };
bool longPress = false;
while (System::Update())
{
{
// 長押しで原点の方角を向く
if (MouseL.down())
{
longPress = true;
}
else if (MouseL.pressed())
{
if (not Cursor::Delta().isZero())
{
longPress = false;
}
else if (longPress && (MouseL.pressedDuration() >= 0.8s))
{
longPress = false;
if (const auto eyePos = camera.getEyePosition().xz(); not eyePos.isZero())
{
const auto focusPos = camera.getFocusPosition().xz();
const auto focusVec = ((focusPos == eyePos) ? camera.getUpDirection().xz() : (focusPos - eyePos));
const double offsetAlpha = (camera.getOffset().alpha + focusVec.getAngle(-eyePos));
camera.setOffset(Rotation{ offsetAlpha, 0, 0 });
}
}
}
camera.updateOrientation();
camera.updateTouchUI();
Graphics3D::SetCameraTransform(camera);
}
{
const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
{
// 地面の描画
groundPlane.draw(groundTexture);
// 地球の描画
earthSphere.draw(earthTexture);
// 鍛冶屋の描画
blacksmithModel.draw(Vec3{ 8, 0, 4 });
// 風車の描画
DrawMillModel(millModel, Mat4x4::Translate(-8, 0, 4));
// 木の描画
{
const ScopedRenderStates3D renderStates{ BlendState::OpaqueAlphaToCoverage, RasterizerState::SolidCullNone };
treeModel.draw(Vec3{ 16, 0, 4 });
pineModel.draw(Vec3{ 16, 0, 0 });
}
// Siv3D くんの描画
siv3dkunModel.draw(Vec3{ 2, 0, -2 }, Quaternion::RotateY(180_deg));
}
}
{
Graphics3D::Flush();
renderTexture.resolve();
Shader::LinearToScreen(renderTexture);
}
{
Rect{ 0, 0, 500, 300 }.draw(ColorF{ 0, 0.8 });
auto x, y, z = DeviceMotion::Acceleration(); font(U"acceleration.x: {:6.2f}"_fmt(x)).draw(30, 20, 20, Palette::White);
font(U"acceleration.y: {:6.2f}"_fmt(y)).draw(30, 20, 110, Palette::White);
font(U"acceleration.z: {:6.2f}"_fmt(z)).draw(30, 20, 200, Palette::White);
Rect{ 50, 70, 400, 20 }.draw(Palette::Lightgray);
RectF{ 250, 70, Clamp(x * 10, -200.0, 200.0), 20 }.draw(Palette::Orange);
Rect{ 50, 160, 400, 20 }.draw(Palette::Lightgray);
RectF{ 250, 160, Clamp(y * 10, -200.0, 200.0), 20 }.draw(Palette::Orange);
Rect{ 50, 250, 400, 20 }.draw(Palette::Lightgray);
RectF{ 250, 250, Clamp(z * 10, -200.0, 200.0), 20 }.draw(Palette::Orange);
}
{
camera.drawTouchUI();
}
}
}