最初に RoomPlan を触ったとき、同じ六畳の部屋を三回スキャンして、三回とも壁の長さが違う数字で返ってきました。3.62m、3.57m、3.64m。誤差にして数センチですが、間取り図に書き出すアプリを作るなら、この揺れをそのまま見せるわけにはいきません。利用者は「測るたびに数字が変わる道具」を信用しないからです。
Rork Max が Swift ネイティブを生成できるようになって、LiDAR を前提にした計測アプリが個人開発でも一気に近づきました。ただ、生成されたコードをそのまま公開すると、この「揺れ」が利用者の手元でむき出しになります。今回は、私自身が小さな採寸アプリを通して整理した、スキャン精度のばらつきを設計で吸収する考え方を共有します。
なぜ RoomPlan の数字は揺れるのか
RoomPlan は ARKit の上に載っていて、LiDAR で取った点群と、カメラ画像から推定した平面を組み合わせて部屋の構造を再構成します。つまり最終的な寸法は「測定」ではなく「推定」です。推定である以上、入力条件で結果が動きます。
揺れの主な原因は三つあります。採光です。逆光や暗所では特徴点が拾えず、平面推定が甘くなります。次に移動速度です。速く振ると点群が疎になり、壁の端の確定が遅れます。そして端末です。iPhone と iPad Pro では LiDAR の解像度もカメラの画角も違い、同じ部屋でも再構成が微妙にずれます。
ここで大事なのは、これらを「バグ」として潰そうとしないことです。推定の揺れは仕様であり、アプリ側が受け止める前提で設計するほうが、結果として安定します。
揺れを受け止める三層の設計
私は計測データを三つの層に分けて扱うことにしています。生データ層、確定層、表示層です。
生データ層は RoomPlan が返す CapturedRoom をそのまま保持します。ここでは一切丸めません。確定層では、生データに丸めとスナップをかけて「アプリが責任を持つ寸法」に変換します。表示層は確定層の値だけを利用者に見せます。
この分離をしておくと、後で丸めの基準を変えたくなったときに、生データを捨てずにやり直せます。スキャンは利用者にとって手間のかかる操作なので、撮り直しを減らせる設計は体験に直結します。
確定層でかける丸めとスナップ
確定層の中心は二つの処理です。一つは 5cm 単位への丸め。もう一つは直角・平行へのスナップです。実際の部屋の壁はほぼ直角と平行で構成されているので、推定が 88 度や 92 度を返してきたら 90 度に寄せたほうが図面として自然になります。
import RoomPlan
import simd
struct ConfirmedDimension {
let widthMeters: Double
let lengthMeters: Double
let cornerAngles: [Double] // 各コーナーの角度(度)
}
enum DimensionResolver {
// 5cm 単位に丸める。利用者が信頼できる粒度に揃える
static func snapLength(_ raw: Double) -> Double {
let grid = 0.05
return (raw / grid).rounded() * grid
}
// 直角・平行に寄せる。±4度以内なら 90度倍数へスナップ
static func snapAngle(_ rawDeg: Double) -> Double {
let nearest = (rawDeg / 90.0).rounded() * 90.0
return abs(rawDeg - nearest) <= 4.0 ? nearest : rawDeg
}
static func resolve(from room: CapturedRoom) -> ConfirmedDimension {
let walls = room.walls
// 床平面の最長辺・直交辺をおおまかに width / length とみなす簡略版
let lengths = walls.map { Double($0.dimensions.x) }.sorted(by: >)
let width = snapLength(lengths.first ?? 0)
let length = snapLength(lengths.dropFirst().first ?? 0)
let angles = walls.map { wall -> Double in
let yaw = atan2(wall.transform.columns.0.z, wall.transform.columns.0.x)
return snapAngle(yaw * 180 / .pi)
}
return ConfirmedDimension(widthMeters: width, lengthMeters: length, cornerAngles: angles)
}
}ここで ±4度以内 というしきい値は、私の手元の端末で何度かスキャンして決めた経験値です。広い角度まで寄せると、本当に斜めの壁がある部屋で図面が崩れます。狭すぎると揺れを吸収しきれません。読者のアプリが扱う部屋の種類に合わせて、ここは調整する前提で持ってください。
信頼度を一緒に持たせる
丸めた数字だけを渡すと、表示層は「どれくらい信じてよいか」を判断できません。私は確定値に信頼度スコアを添えるようにしています。点群の密度とスキャン時間から雑に算出するだけでも、表示の出し分けに使えます。
struct DimensionWithConfidence {
let dimension: ConfirmedDimension
let confidence: Double // 0.0〜1.0
var needsRescanHint: Bool { confidence < 0.6 }
}needsRescanHint が立っていたら、表示層で「もう一度ゆっくり撮ると精度が上がります」と添えます。数字を隠すのではなく、確からしさを正直に伝えるほうが、利用者は道具を信頼してくれます。