175 lines
5.4 KiB
Markdown
175 lines
5.4 KiB
Markdown
---
|
|
id: wiki-20260508-vr-sickness-redir
|
|
title: VR Sickness
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [cybersickness, simulator sickness, VR motion sickness]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.91
|
|
verification_status: applied
|
|
tags: [vr, ux, perception, three-js]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: TypeScript
|
|
framework: Three.js / WebXR
|
|
---
|
|
|
|
# VR Sickness
|
|
|
|
## 매 한 줄
|
|
> **"매 visual motion 과 vestibular 의 mismatch 의 motion sickness 의 trigger"**. 매 VR 의 가장 큰 UX 장벽 — 매 frame rate / FOV / locomotion 의 careful design 의 mitigation. 2026 의 Quest 3 / Vision Pro / PCVR 의 90Hz+ 의 default — 매 여전히 design pattern 의 핵심.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 원인
|
|
- **Sensory mismatch**: 매 eye 의 motion 의 perceive — 매 inner ear 의 stationary 의 report.
|
|
- **Low frame rate**: <72fps 의 judder 의 sickness 의 trigger.
|
|
- **Vection**: 매 large optical flow 의 self-motion 의 illusion.
|
|
- **Latency**: motion-to-photon >20ms 의 mismatch 의 amplify.
|
|
- **FOV motion**: peripheral 의 motion 의 sensitivity 가장 큼.
|
|
|
|
### 매 mitigation 기법
|
|
- **Teleport locomotion**: 매 smooth 대신 fade-to-black + jump.
|
|
- **Tunnel vision (vignette)**: 매 motion 시 peripheral mask — 매 vection 감소.
|
|
- **Snap turning**: 매 smooth rotation 대신 30° step.
|
|
- **Comfort settings**: 매 user 의 individual tuning.
|
|
- **High frame rate**: 90Hz+ 의 mandatory — Quest 3 의 default 90/120Hz.
|
|
- **Stable horizon**: 매 cockpit / fixed reference frame.
|
|
|
|
### 매 응용
|
|
1. Beat Saber — 매 stationary play 의 zero motion sickness.
|
|
2. Half-Life Alyx — 매 teleport + smooth 의 toggle.
|
|
3. 매 자전거 simulator — 매 physical motion 의 real vestibular alignment.
|
|
|
|
## 💻 패턴
|
|
|
|
### 매 Three.js + WebXR 의 framerate 의 monitor
|
|
```typescript
|
|
import * as THREE from 'three';
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.xr.enabled = true;
|
|
|
|
let lastTime = performance.now();
|
|
renderer.setAnimationLoop((time) => {
|
|
const dt = time - lastTime;
|
|
if (dt > 14) console.warn(`Frame drop: ${dt.toFixed(1)}ms`); // 매 <72fps 의 경고
|
|
lastTime = time;
|
|
renderer.render(scene, camera);
|
|
});
|
|
```
|
|
|
|
### 매 Vignette 의 motion 시 적용
|
|
```glsl
|
|
// fragment shader
|
|
uniform float u_vignetteStrength;
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vec2 center = vUv - 0.5;
|
|
float dist = length(center);
|
|
float vignette = smoothstep(0.5, 0.3 - u_vignetteStrength * 0.2, dist);
|
|
gl_FragColor = vec4(color.rgb * vignette, 1.0);
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// 매 movement 의 detect 후 strength 의 ramp
|
|
const speed = velocity.length();
|
|
material.uniforms.u_vignetteStrength.value = THREE.MathUtils.clamp(speed / 5, 0, 0.6);
|
|
```
|
|
|
|
### 매 Snap turn 의 implementation
|
|
```typescript
|
|
let lastTurnTime = 0;
|
|
const SNAP_ANGLE = Math.PI / 6; // 30°
|
|
const COOLDOWN = 250;
|
|
|
|
function update(controller: THREE.Group, input: GamepadAxes) {
|
|
const now = performance.now();
|
|
if (Math.abs(input.thumbstickX) > 0.7 && now - lastTurnTime > COOLDOWN) {
|
|
rig.rotation.y -= Math.sign(input.thumbstickX) * SNAP_ANGLE;
|
|
lastTurnTime = now;
|
|
fadeOutIn(50); // 매 brief blackout 의 ease
|
|
}
|
|
}
|
|
```
|
|
|
|
### 매 Teleport locomotion
|
|
```typescript
|
|
function teleport(targetPos: THREE.Vector3) {
|
|
fadeToBlack(150).then(() => {
|
|
rig.position.copy(targetPos);
|
|
fadeFromBlack(150);
|
|
});
|
|
}
|
|
```
|
|
|
|
### 매 Stable horizon (cockpit reference)
|
|
```typescript
|
|
// 매 vehicle simulator 의 cockpit mesh 의 always-visible
|
|
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
|
|
camera.add(cockpit); // 매 head 에 follow — 매 vestibular reference
|
|
scene.add(camera);
|
|
```
|
|
|
|
### 매 Comfort 설정 의 노출
|
|
```typescript
|
|
const settings = {
|
|
movementType: 'teleport' as 'teleport' | 'smooth',
|
|
vignetteEnabled: true,
|
|
snapTurn: true,
|
|
snapAngle: 30,
|
|
};
|
|
// 매 in-game menu 의 user 노출 — 매 individual variance 의 대응
|
|
```
|
|
|
|
### 매 Latency 의 측정
|
|
```typescript
|
|
const xrSession = renderer.xr.getSession();
|
|
xrSession?.requestAnimationFrame((time, frame) => {
|
|
// 매 frame.predictedDisplayTime - performance.now() = motion-to-photon
|
|
});
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Casual user | teleport + snap turn (default) |
|
|
| Hardcore VR | smooth + comfort toggle |
|
|
| Vehicle sim | cockpit + stable horizon |
|
|
| Stationary game | minimal locomotion (Beat Saber) |
|
|
| Motion ride | physical motion 의 sync |
|
|
|
|
**기본값**: teleport + snap turn 의 default. 매 user toggle 의 expose.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Virtual Reality UX]]
|
|
- 변형: [[VR 멀미 (VR Sickness)]] · [[VR 멀미(VR sickness)]] (Korean variants)
|
|
- 응용: [[Beat Saber]] · [[가상현실(VR) 자전거 시뮬레이터]]
|
|
- Adjacent: [[Vergence-Accommodation Conflicts]] · [[깊이 지각(Depth perception)]] · [[Edge Bleeding]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: VR app 의 design 의 comfort 권장, frame rate budget 의 explain, snap turn / teleport 의 trade-off.
|
|
**언제 X**: 매 medical 진단 — 매 individual variance 의 큼 의 인지.
|
|
|
|
## ❌ 안티패턴
|
|
- **<72fps 의 ship**: 매 sickness 의 garantee.
|
|
- **Smooth-only locomotion**: 매 casual user 의 alienate.
|
|
- **Forced camera shake**: 매 vection 증폭.
|
|
- **Unstable horizon**: 매 vehicle 의 wobble.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (Oculus VR Best Practices, Valve Half-Life Alyx postmortem).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — VR sickness FULL 작성 |
|