diff --git a/threejs/lessons/kr/threejs-webvr-look-to-select.md b/threejs/lessons/kr/threejs-webvr-look-to-select.md new file mode 100644 index 00000000..0ed71000 --- /dev/null +++ b/threejs/lessons/kr/threejs-webvr-look-to-select.md @@ -0,0 +1,430 @@ +Title: Three.js VR - Look to Select +Description: Look to Select을 시행하는 방법 +TOC: VR - Look To Select + +**NOTE: 이 페이지의 예시에는 VR 지원 기기가 필요합니다. +VR 기기 없이는 동작하지 않으며 그 이유를 [이전 글](threejs-webvr.html) +에서 확인할 수 있습니다.** + +[이전 글](threejs-webvr.html)에서 우리는 three.js를 사용한 매우 간단한 VR 예제를 살펴보고 다양한 종류의 VR 시스템에 대해 이야기했습니다. + +가장 간단하고 흔한 것은 기본적으로 5달러에서 50달러의 얼굴 마스크에 넣는 전화기인 VR 구글 카드 보드 스타일입니다. +이런 종류의 VR에는 컨트롤러가 없기 때문에 사람들은 사용자 입력을 허용하기 위한 창의적인 해결책을 생각해 내야 합니다. + +이때 가장 일반적인 해결책은 사용자가 무언가를 잠시 동안 가리킬 경우 그것이 선택되는 "Look to Select"입니다. + +"Look to Select"를 구현해봅시다! 먼저 [이전 글의 예시](threejs-webvr.html)에서 시작해 [Three.js 피킹](threejs-picking.html)에서 만든 `PickHelper`를 추가할 것입니다. + +```js +class PickHelper { + constructor() { + this.raycaster = new THREE.Raycaster(); + this.pickedObject = null; + this.pickedObjectSavedColor = 0; + } + pick(normalizedPosition, scene, camera, time) { + // restore the color if there is a picked object + if (this.pickedObject) { + this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor); + this.pickedObject = undefined; + } + + // cast a ray through the frustum + this.raycaster.setFromCamera(normalizedPosition, camera); + // get the list of objects the ray intersected + const intersectedObjects = this.raycaster.intersectObjects(scene.children); + if (intersectedObjects.length) { + // pick the first object. It's the closest one + this.pickedObject = intersectedObjects[0].object; + // save its color + this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex(); + // set its emissive color to flashing red/yellow + this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000); + } + } +} +``` + +해당 코드에 대한 설명은 [피킹에 대한 글](threejs-picking.html)을 참조하십시오. + +이 기능을 사용하려면 인스턴스를 만들고 render loop에서 호출하기만 하면 됩니다. + +```js ++const pickHelper = new PickHelper(); + +... +function render(time) { + time *= 0.001; + + ... + ++ // 0, 0 is the center of the view in normalized coordinates. ++ pickHelper.pick({x: 0, y: 0}, scene, camera, time); +``` + +원래의 피킹 예시에서 우리는 마우스 좌표를 CSS 픽셀에서 캔버스를 가로질러 -1에서 +1로 가는 정규화된 좌표로 변환했습니다. + +이 경우 우리는 항상 카메라가 마주 보고 있는 화면의 중심을 선택하기 때문에 정규화된 좌표의 중심인 x와 y 모두에 대해 0을 통과합니다. + +그리고 우리가 그 물체들을 볼 때 그 물체들은 번쩍거릴 것입니다. + +{{{example url="../threejs-webvr-look-to-select.html" }}} + +일반적으로 우리는 즉각적인 선택을 원하지 않습니다. + +대신 우리는 실수로 어떤 것을 선택하지 않도록 하기 위해 몇 분 동안 카메라를 사용자가 선택하고자 하는 것에 고정시키도록 합니다. + +그러기 위해서 사용자가 계속 보고 있었는지, 그리고 얼마나 오래 있었는지를 전달하기 위한 일종의 미터나 게이지나 방법이 필요합니다. + +이를 위한 한 가지 쉬운 방법은 2가지 색상의 텍스처를 만들고 텍스처 오프셋을 사용하여 모델을 가로질러 텍스처를 이동시키는 것입니다. + +VR 예제에 추가하기 전에 스스로 작동하는지 보도록 합시다. + +먼저 `OrthographicCamera`를 만들어 보겠습니다. + +```js +const left = -2; // Use values for left +const right = 2; // right, top and bottom +const top = 1; // that match the default +const bottom = -1; // canvas size. +const near = -1; +const far = 1; +const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far); +``` + +그리고 캔버스의 크기가 변경되면 업데이트하는 것을 잊지 마십시오. + +```js +function render(time) { + time *= 0.001; + + if (resizeRendererToDisplaySize(renderer)) { + const canvas = renderer.domElement; + const aspect = canvas.clientWidth / canvas.clientHeight; ++ camera.left = -aspect; ++ camera.right = aspect; + camera.updateProjectionMatrix(); + } + ... +``` + +우리는 현재 중앙 위아래 두 유닛과 좌우 측면 유닛을 보여주는 카메라를 가지고 있습니다. + +다음으로 2가지 색 텍스처를 만들어 봅시다. 몇 군데 [다른](threejs-indexed-textures.html) [곳에서](threejs-post-processing-3dlut.html) 사용했던 `DataTexture`를 사용할 것입니다. + +```js +function makeDataTexture(data, width, height) { + const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat); + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + texture.needsUpdate = true; + return texture; +} + +const cursorColors = new Uint8Array([ + 64, 64, 64, 64, // dark gray + 255, 255, 255, 255, // white +]); +const cursorTexture = makeDataTexture(cursorColors, 2, 1); +``` + +그 다음 `TorusGeometry`에 있는 텍스처를 사용할 것입니다. + +```js +const ringRadius = 0.4; +const tubeRadius = 0.1; +const tubeSegments = 4; +const ringSegments = 64; +const cursorGeometry = new THREE.TorusGeometry( + ringRadius, tubeRadius, tubeSegments, ringSegments); + +const cursorMaterial = new THREE.MeshBasicMaterial({ + color: 'white', + map: cursorTexture, + transparent: true, + blending: THREE.CustomBlending, + blendSrc: THREE.OneMinusDstColorFactor, + blendDst: THREE.OneMinusSrcColorFactor, +}); +const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial); +scene.add(cursor); +``` + +그 다음 `render`에서 텍스처의 오프셋을 조정하도록 합니다. + +```js +function render(time) { + time *= 0.001; + + if (resizeRendererToDisplaySize(renderer)) { + const canvas = renderer.domElement; + const aspect = canvas.clientWidth / canvas.clientHeight; + camera.left = -aspect; + camera.right = aspect; + camera.updateProjectionMatrix(); + } + ++ const fromStart = 0; ++ const fromEnd = 2; ++ const toStart = -0.5; ++ const toEnd = 0.5; ++ cursorTexture.offset.x = THREE.MathUtils.mapLinear( ++ time % 2, ++ fromStart, fromEnd, ++ toStart, toEnd); + + renderer.render(scene, camera); +} +``` + +`THREE.MathUtils.mapLinear`는 `fromStart`와 `fromEnd` 사이의 값을 취하여 시작과 끝 사이의 값으로 매핑합니다. + +위의 경우, 0에서 2까지의 값을 의미하는 `time % 2`를 취하여 -0.5에서 0.5까지의 값에 매핑합니다. + +[텍스처](threejs-textures.html)는 0에서 1까지 정규화된 텍스처 좌표를 사용하여 geometry에 매핑됩니다. +즉, 기본 래핑 모드인 `THREE.ClampToEdge`로 설정된 2x1 픽셀 이미지를 의미하며, +텍스처 좌표를 -0.5만큼 조정하면 전체 메시가 첫 번째 색상이 되고 텍스처 좌표를 +0.5만큼 조정하면 전체 메시가 두 번째 색상이 됩니다. +필터링을 `THREE.NearestFilter`로 설정하면 geometry를 통해 두 색상 간의 전환이 가능해집니다. + +[배경과 관련된 글](threejs-backgrounds.html)에서 다루었던 것처럼 배경의 질감을 더해봅시다. +2x2 색상 셋을 사용하지만 텍스처의 반복 설정을 8x8 그리드로 설정할 수 있습니다. +이렇게 하면 커서가 렌더링 되어 다른 색상과 대조하여 확인할 수 있습니다. + +```js ++const backgroundColors = new Uint8Array([ ++ 0, 0, 0, 255, // black ++ 90, 38, 38, 255, // dark red ++ 100, 175, 103, 255, // medium green ++ 255, 239, 151, 255, // light yellow ++]); ++const backgroundTexture = makeDataTexture(backgroundColors, 2, 2); ++backgroundTexture.wrapS = THREE.RepeatWrapping; ++backgroundTexture.wrapT = THREE.RepeatWrapping; ++backgroundTexture.repeat.set(4, 4); + +const scene = new THREE.Scene(); ++scene.background = backgroundTexture; +``` + +이제 이것을 실행하면 게이지와 같은 원을 얻을 수 있고 게이지 위치를 설정할 수 있습니다. + +{{{example url="../threejs-webvr-look-to-select-selector.html" }}} + +몇 가지 주목하고 **시도해야 할 것들**이 있습니다. + +* 다음과 같이 `cursorMaterial`의 `blending`, `blendSrc`, `blendDst` + 속성을 설정합니다. + + blending: THREE.CustomBlending, + blendSrc: THREE.OneMinusDstColorFactor, + blendDst: THREE.OneMinusSrcColorFactor, + + 이것은 효과의 *역* 타입으로 주어집니다. + 그 세 줄에 주석을 달면 차이를 알 수 있을 것입니다. + 저는 역효과가 가장 좋다고 생각하는데, 이렇게 하면 커서의 색깔에 상관없이 커서가 보일 수 있기 때문입니다. + +* `RingGeometry`가 아닌 `TorusGeometry`를 사용해 봅시다. + + 어떤 이유로든 `RingGeometry`는 평평한 UV 매핑 방식을 사용합니다. + 이 때문에 `RingGeometry`를 사용하면 위에서처럼 링 주위가 아닌 수평으로 링을 가로질러 텍스처가 미끄러집니다. + + 이걸 시도해 보고 `TorusGeometry`를 `RingGeometry`(위 예시에서 설명한 대로)로 바꾸면 무슨 뜻인지 알 수 있을 것입니다. + + *적절한* 정의를 위한 *적절한* 할 것은 `RingGeometry`를 사용하되 링 주위를 돌도록 텍스처 좌표를 고정하는 것입니다. + 아니면, 자신만의 링 지오메트리를 생성하세요. 그래도 torus는 잘 작동합니다. + `MeshBasicMaterial`과 함께 카메라 바로 앞에 배치하면 링과 똑같이 보이고 텍스처 좌표가 링 주위를 돌기 때문에 우리가 원하는 대로 작동합니다. + +이제 이것을 위의 VR 코드와 통합해 봅시다. + +```js +class PickHelper { +- constructor() { ++ constructor(camera) { + this.raycaster = new THREE.Raycaster(); + this.pickedObject = null; +- this.pickedObjectSavedColor = 0; + ++ const cursorColors = new Uint8Array([ ++ 64, 64, 64, 64, // dark gray ++ 255, 255, 255, 255, // white ++ ]); ++ this.cursorTexture = makeDataTexture(cursorColors, 2, 1); ++ ++ const ringRadius = 0.4; ++ const tubeRadius = 0.1; ++ const tubeSegments = 4; ++ const ringSegments = 64; ++ const cursorGeometry = new THREE.TorusGeometry( ++ ringRadius, tubeRadius, tubeSegments, ringSegments); ++ ++ const cursorMaterial = new THREE.MeshBasicMaterial({ ++ color: 'white', ++ map: this.cursorTexture, ++ transparent: true, ++ blending: THREE.CustomBlending, ++ blendSrc: THREE.OneMinusDstColorFactor, ++ blendDst: THREE.OneMinusSrcColorFactor, ++ }); ++ const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial); ++ // add the cursor as a child of the camera ++ camera.add(cursor); ++ // and move it in front of the camera ++ cursor.position.z = -1; ++ const scale = 0.05; ++ cursor.scale.set(scale, scale, scale); ++ this.cursor = cursor; ++ ++ this.selectTimer = 0; ++ this.selectDuration = 2; ++ this.lastTime = 0; + } + pick(normalizedPosition, scene, camera, time) { ++ const elapsedTime = time - this.lastTime; ++ this.lastTime = time; + +- // restore the color if there is a picked object +- if (this.pickedObject) { +- this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor); +- this.pickedObject = undefined; +- } + ++ const lastPickedObject = this.pickedObject; ++ this.pickedObject = undefined; + + // cast a ray through the frustum + this.raycaster.setFromCamera(normalizedPosition, camera); + // get the list of objects the ray intersected + const intersectedObjects = this.raycaster.intersectObjects(scene.children); + if (intersectedObjects.length) { + // pick the first object. It's the closest one + this.pickedObject = intersectedObjects[0].object; +- // save its color +- this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex(); +- // set its emissive color to flashing red/yellow +- this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000); + } + ++ // show the cursor only if it's hitting something ++ this.cursor.visible = this.pickedObject ? true : false; ++ ++ let selected = false; ++ ++ // if we're looking at the same object as before ++ // increment time select timer ++ if (this.pickedObject && lastPickedObject === this.pickedObject) { ++ this.selectTimer += elapsedTime; ++ if (this.selectTimer >= this.selectDuration) { ++ this.selectTimer = 0; ++ selected = true; ++ } ++ } else { ++ this.selectTimer = 0; ++ } ++ ++ // set cursor material to show the timer state ++ const fromStart = 0; ++ const fromEnd = this.selectDuration; ++ const toStart = -0.5; ++ const toEnd = 0.5; ++ this.cursorTexture.offset.x = THREE.MathUtils.mapLinear( ++ this.selectTimer, ++ fromStart, fromEnd, ++ toStart, toEnd); ++ ++ return selected ? this.pickedObject : undefined; + } +} +``` + +위의 코드를 보시면 커서 형상, 텍스처, 매테리얼을 만들기 위해 모든 코드를 추가한 것을 볼 수 있습니다. +그리고 카메라의 자식으로 추가해서 항상 카메라 앞에 놓이게 합니다. +커서가 렌더링 되지 않을 경우 카메라를 scene에 추가해야 합니다. + +```js ++scene.add(camera); +``` + +이 다음 이번에 피킹 할 것이 지난번과 같은지 확인합니다. +타이머에 경과 시간을 추가하고 타이머가 한계치에 도달하면 선택한 항목을 반환합니다. + +이제 큐브들을 고르는데 그것을 사용해 봅시다. +간단한 예로 3개의 구도 추가하겠습니다. +큐브를 선택하여 큐브를 숨기고 해당 구의 숨기기를 취소합니다. + +먼저 구면 geometry를 만들어보겠습니다. + +```js +const boxWidth = 1; +const boxHeight = 1; +const boxDepth = 1; +-const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth); ++const boxGeometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth); ++ ++const sphereRadius = 0.5; ++const sphereGeometry = new THREE.SphereGeometry(sphereRadius); +``` + +그리고 세 쌍의 박스와 구 `Mesh`를 만들어 봅시다. 각 `Mesh`를 파트너와 연결할 수 있도록 [`맵`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)을 사용할 것입니다. + +```js +-const cubes = [ +- makeInstance(geometry, 0x44aa88, 0), +- makeInstance(geometry, 0x8844aa, -2), +- makeInstance(geometry, 0xaa8844, 2), +-]; ++const meshToMeshMap = new Map(); ++[ ++ { x: 0, boxColor: 0x44aa88, sphereColor: 0xFF4444, }, ++ { x: 2, boxColor: 0x8844aa, sphereColor: 0x44FF44, }, ++ { x: -2, boxColor: 0xaa8844, sphereColor: 0x4444FF, }, ++].forEach((info) => { ++ const {x, boxColor, sphereColor} = info; ++ const sphere = makeInstance(sphereGeometry, sphereColor, x); ++ const box = makeInstance(boxGeometry, boxColor, x); ++ // hide the sphere ++ sphere.visible = false; ++ // map the sphere to the box ++ meshToMeshMap.set(box, sphere); ++ // map the box to the sphere ++ meshToMeshMap.set(sphere, box); ++}); +``` + +큐브를 회전하는 `render`에서 `cubes` 대신 `meshToMeshMap`를 반복해야 합니다. + +```js +-cubes.forEach((cube, ndx) => { ++let ndx = 0; ++for (const mesh of meshToMeshMap.keys()) { + const speed = 1 + ndx * .1; + const rot = time * speed; +- cube.rotation.x = rot; +- cube.rotation.y = rot; +-}); ++ mesh.rotation.x = rot; ++ mesh.rotation.y = rot; ++ ++ndx; ++} +``` + +이제 새로운 `PickHelper` 구현을 사용하여 개체 중 하나를 선택할 수 있습니다. 이 옵션을 선택하면 개체를 숨기고 그 파트너를 드러냅니다. + +```js +// 0, 0 is the center of the view in normalized coordinates. +-pickHelper.pick({x: 0, y: 0}, scene, camera, time); ++const selectedObject = pickHelper.pick({x: 0, y: 0}, scene, camera, time); ++if (selectedObject) { ++ selectedObject.visible = false; ++ const partnerObject = meshToMeshMap.get(selectedObject); ++ partnerObject.visible = true; ++} +``` + +그리고 이를 통해 우리는 꽤 괜찮은 *look to select*를 구현해야 합니다. + +{{{example url="../threejs-webvr-look-to-select-w-cursor.html" }}} + +이 예제가 구글 카드 보드 레벨 UX의 "look to select"를 구현하는 방법에 대한 아이디어를 주었기를 바랍니다. +텍스쳐 좌표 오프셋을 사용한 슬라이딩 텍스쳐도 일반적으로 유용한 기법입니다. + +다음으로는 [VR 컨트롤러가 있는 사용자가 사물을 가리키고 이동할 수 있는 방법을 알아보겠습니다.](threejs-webvr-point-to-select.html). diff --git a/threejs/lessons/kr/threejs-webvr-point-to-select.md b/threejs/lessons/kr/threejs-webvr-point-to-select.md new file mode 100644 index 00000000..01cb963e --- /dev/null +++ b/threejs/lessons/kr/threejs-webvr-point-to-select.md @@ -0,0 +1,356 @@ +Title: Three.js VR - 3DOF Point to Select +Description: 3DOF Point to Select를 시행하는 방법 +TOC: VR - Point To Select + +**NOTE: 이 페이지의 예시에는 포인팅 장치가 있는 VR 지원 장치가 필요합니다. +포인팅 장치가 있는 VR 지원 장치가 없으면 작업을 수행할 수 없으며 +그 이유를 [이 글에서](threejs-webvr.html) 확인할 수 있습니다. +** + +[이전 글](threejs-webvr-look-to-select.html)에서는 사용자가 보는 것을 통해 항목을 가리키며 선택할 수 있도록 하는 매우 간단한 VR 예제를 살펴보았습니다. +이 글에서는 한 단계 더 나아가 사용자가 포인팅 장치를 사용하여 항목을 선택할 수 있도록 해보겠습니다. + +Three.js는 2개의 컨트롤러 개체를 VR로 제공하여 비교적 쉽게 만들며 단일 3DOF 컨트롤러와 2개의 6DOF 컨트롤러의 경우를 모두 처리하려고 합니다. +각 컨트롤러는 컨트롤러의 방향과 위치를 제공하는 `Object3D` 개체입니다. +또한 사용자가 컨트롤러의 "메인" 버튼을 누르기 시작하고, 누르고, 누르기를 중지할 때(끝낼 때) `selectstart`, `select` 및 `selectend` 이벤트를 제공합니다. + +[이전 글](threejs-webvr-look-to-select.html)의 마지막 예시에서 `PickHelper`를 `ControllerPickHelper`로 변경해 보겠습니다. + +이번의 새로운 구현에서는 선택된 개체를 제공하는 `select` 이벤트를 내보낼 것입니다. +따라서 개체를 사용하기 위해 이 작업을 수행해야 합니다. + +```js +const pickHelper = new ControllerPickHelper(scene); +pickHelper.addEventListener('select', (event) => { + event.selectedObject.visible = false; + const partnerObject = meshToMeshMap.get(event.selectedObject); + partnerObject.visible = true; +}); +``` + +이전의 코드를 떠올려 보면 상자와 구를 서로 매핑하면 `meshToMeshMap`를 통해 박스와 구를 찾을 수 있으므로 +여기서는 선택된 개체를 숨기고 파트너의 숨김을 해제합니다. + + `ControllerPickHelper`의 실제 구현에 대해서는 먼저 VR 컨트롤러 개체를 scene에 추가하고 + 이러한 개체에 사용자가 가리키는 위치를 표시하는 데 사용할 수 있는 3D 라인을 추가하고, 컨트롤러와 라인을 모두 저장해야 합니다. + +```js +class ControllerPickHelper { + constructor(scene) { + const pointerGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + this.controllers = []; + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); + scene.add(controller); + + const line = new THREE.Line(pointerGeometry); + line.scale.z = 5; + controller.add(line); + this.controllers.push({controller, line}); + } + } +} +``` + +다른 어떠한 작업을 수행하지 않고 이 작업만으로도 +사용자의 포인팅 장치가 어디에 있고 어느 쪽을 가리키고 있는지를 보여주는 scene에서 한 두개의 라인이 제공됩니다. + +다음에는 컨트롤러로 선택하는 코드를 추가해봅시다. 카메라가 아닌 것으로 선택하는 것은 이번이 처음입니다. +[피킹에 관한 글](threejs-picking.html)에서는 마우스나 손가락을 사용하여 선택하는 것이 카메라에서 화면으로 전달된다는 것을 의미했습니다. +[이전 글](threejs-webvr-look-to-select.html)에서는 카메라에 나오는 사용자가 어떤 식으로 다시 보이는지를 기준으로 선택했습니다. +이번에는 카메라를 사용하지 않기 때문에 컨트롤러의 위치를 통해 선택합니다. + +```js +class ControllerPickHelper { + constructor(scene) { ++ this.raycaster = new THREE.Raycaster(); ++ this.objectToColorMap = new Map(); ++ this.controllerToObjectMap = new Map(); ++ this.tempMatrix = new THREE.Matrix4(); + + const pointerGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + this.controllers = []; + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); + scene.add(controller); + + const line = new THREE.Line(pointerGeometry); + line.scale.z = 5; + controller.add(line); + this.controllers.push({controller, line}); + } + } ++ update(scene, time) { ++ this.reset(); ++ for (const {controller, line} of this.controllers) { ++ // cast a ray through the from the controller ++ this.tempMatrix.identity().extractRotation(controller.matrixWorld); ++ this.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); ++ this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(this.tempMatrix); ++ // get the list of objects the ray intersected ++ const intersections = this.raycaster.intersectObjects(scene.children); ++ if (intersections.length) { ++ const intersection = intersections[0]; ++ // make the line touch the object ++ line.scale.z = intersection.distance; ++ // pick the first object. It's the closest one ++ const pickedObject = intersection.object; ++ // save which object this controller picked ++ this.controllerToObjectMap.set(controller, pickedObject); ++ // highlight the object if we haven't already ++ if (this.objectToColorMap.get(pickedObject) === undefined) { ++ // save its color ++ this.objectToColorMap.set(pickedObject, pickedObject.material.emissive.getHex()); ++ // set its emissive color to flashing red/yellow ++ pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFF2000 : 0xFF0000); ++ } ++ } else { ++ line.scale.z = 5; ++ } ++ } ++ } +} +``` + +`Raycaster`를 사용하기 전에는 그랬지만 이번에는 컨트롤러에서 ray를 가져옵니다. +이전 `PickHelper`에서는 한 가지만 선택할 수 있었지만, 여기서는 한 손에 하나씩 최대 2개의 컨트롤러가 있습니다. +우리는 각 컨트롤러가 보고 있는 개체를 `controllerToObjectMap`에 저장해야 합니다. +또한 원래 방사체의 색을 `objectToColorMap`에 저장하여 선이 가리키는 모든 부분에 닿을 수 있을 만큼 길게 만듭니다. + +모든 프레임에서 이러한 설정들을 재설정하려면 몇 가지 코드를 추가해야 합니다. + + +```js +class ControllerPickHelper { + + ... + ++ _reset() { ++ // restore the colors ++ this.objectToColorMap.forEach((color, object) => { ++ object.material.emissive.setHex(color); ++ }); ++ this.objectToColorMap.clear(); ++ this.controllerToObjectMap.clear(); ++ } + update(scene, time) { ++ this._reset(); + + ... + +} +``` + +다음으로 우리는 사용자가 컨트롤러를 클릭했을 때 `select` 이벤트를 내보내야 합니다. +이를위해 three.js의 `EventDispatcher`를 확장한 후 컨트롤러에서 `select` 이벤트를 확인하고 +해당 컨트롤러가 가리키는 것이 있으면 해당 컨트롤러가 가리키는 `select` 이벤트를 내보냅니다. + + +```js +-class ControllerPickHelper { ++class ControllerPickHelper extends THREE.EventDispatcher { + constructor(scene) { ++ super(); + this.raycaster = new THREE.Raycaster(); + this.objectToColorMap = new Map(); // object to save color and picked object + this.controllerToObjectMap = new Map(); + this.tempMatrix = new THREE.Matrix4(); + + const pointerGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + this.controllers = []; + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); ++ controller.addEventListener('select', (event) => { ++ const controller = event.target; ++ const selectedObject = this.controllerToObjectMap.get(controller); ++ if (selectedObject) { ++ this.dispatchEvent({type: 'select', controller, selectedObject}); ++ } ++ }); + scene.add(controller); + + const line = new THREE.Line(pointerGeometry); + line.scale.z = 5; + controller.add(line); + this.controllers.push({controller, line}); + } + } +} +``` + +이제 render loop에서 `update`만 호출하면 됩니다. + + +```js +function render(time) { + + ... + ++ pickHelper.update(scene, time); + + renderer.render(scene, camera); +} +``` + +그리고 컨트롤러가 있는 VR 장치가 있다고 가정하면 컨트롤러를 사용하여 선택할 수 있습니다. + +{{{example url="../threejs-webvr-point-to-select.html" }}} + +만약 우리가 물체를 움직일 수 있기를 원한다면 어떨까요? + +그것은 비교적 쉽습니다. +컨트롤러의 `select` 수신기 코드를 함수 안으로 이동하여 두가지 이상의 용도로 그것을 사용할 수 있도록 합니다. + +```js +class ControllerPickHelper extends THREE.EventDispatcher { + constructor(scene) { + super(); + + ... + + this.controllers = []; + ++ const selectListener = (event) => { ++ const controller = event.target; ++ const selectedObject = this.controllerToObjectMap.get(event.target); ++ if (selectedObject) { ++ this.dispatchEvent({type: 'select', controller, selectedObject}); ++ } ++ }; + + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); +- controller.addEventListener('select', (event) => { +- const controller = event.target; +- const selectedObject = this.controllerToObjectMap.get(event.target); +- if (selectedObject) { +- this.dispatchEvent({type: 'select', controller, selectedObject}); +- } +- }); ++ controller.addEventListener('select', selectListener); + + ... +``` + +이제 이것을 `selectstart`와 `select` 모두에 사용해 봅시다. + + +```js +class ControllerPickHelper extends THREE.EventDispatcher { + constructor(scene) { + super(); + + ... + + this.controllers = []; + + const selectListener = (event) => { + const controller = event.target; + const selectedObject = this.controllerToObjectMap.get(event.target); + if (selectedObject) { +- this.dispatchEvent({type: 'select', controller, selectedObject}); ++ this.dispatchEvent({type: event.type, controller, selectedObject}); + } + }; + + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); + controller.addEventListener('select', selectListener); + controller.addEventListener('selectstart', selectListener); + + ... +``` + +사용자가 컨트롤러의 버튼을 놓을 때 three.js가 전송하는 `selectend` 이벤트도 전달해 봅시다. + + +```js +class ControllerPickHelper extends THREE.EventDispatcher { + constructor(scene) { + super(); + + ... + + this.controllers = []; + + const selectListener = (event) => { + const controller = event.target; + const selectedObject = this.controllerToObjectMap.get(event.target); + if (selectedObject) { + this.dispatchEvent({type: event.type, controller, selectedObject}); + } + }; + ++ const endListener = (event) => { ++ const controller = event.target; ++ this.dispatchEvent({type: event.type, controller}); ++ }; + + for (let i = 0; i < 2; ++i) { + const controller = renderer.xr.getController(i); + controller.addEventListener('select', selectListener); + controller.addEventListener('selectstart', selectListener); ++ controller.addEventListener('selectend', endListener); + + ... +``` + +이제 코드를 변경하여 `selectstart` 이벤트가 발생하면 선택한 개체를 scene에서 제거하고 컨트롤러의 하위 개체로 만듭니다. +즉, 컨트롤러와 함께 이동합니다. `selectend`이벤트를 받게 되면 다시 scene에 넣을 것입니다. + + +```js +const pickHelper = new ControllerPickHelper(scene); +-pickHelper.addEventListener('select', (event) => { +- event.selectedObject.visible = false; +- const partnerObject = meshToMeshMap.get(event.selectedObject); +- partnerObject.visible = true; +-}); + ++const controllerToSelection = new Map(); ++pickHelper.addEventListener('selectstart', (event) => { ++ const {controller, selectedObject} = event; ++ const existingSelection = controllerToSelection.get(controller); ++ if (!existingSelection) { ++ controllerToSelection.set(controller, { ++ object: selectedObject, ++ parent: selectedObject.parent, ++ }); ++ controller.attach(selectedObject); ++ } ++}); ++ ++pickHelper.addEventListener('selectend', (event) => { ++ const {controller} = event; ++ const selection = controllerToSelection.get(controller); ++ if (selection) { ++ controllerToSelection.delete(controller); ++ selection.parent.attach(selection.object); ++ } ++}); +``` + +한 개체가 선택되면 해당 개체와 원래의 부모 개체가 저장됩니다. +사용자가 작업을 마치면 개체를 다시 돌려놓을 수 있습니다. + +`Object3D.attach`를 사용하여 선택한 개체를 재부모화 합니다. +이러한 기능을 통해 scene에서 객체의 방향과 위치를 변경하지 않고도 객체의 부모를 변경할 수 있습니다. + +그리고 우리는 이를 통해 6DOF컨트롤러로 물체를 이동하거나 3DOF 컨트롤러로 방향을 전환할 수도 있을 것입니다. + +{{{example url="../threejs-webvr-point-to-select-w-move.html" }}} + +솔직하게 말해서 나는 이 `ControllerPickHelper`가 코드를 구성하는 가장 좋은 방법이라고 확신할 수 없습니다. +하지만 이것은 three.js의 VR에서 작동하는 간단한 작업들의 다양한 부분을 보여주는데 유용합니다. diff --git a/threejs/lessons/kr/threejs-webvr.md b/threejs/lessons/kr/threejs-webvr.md new file mode 100644 index 00000000..e9b8ab6f --- /dev/null +++ b/threejs/lessons/kr/threejs-webvr.md @@ -0,0 +1,385 @@ +Title: Three.js VR +Description: 가상현실을 Three.js로 사용하는 방법. +TOC: VR - 기본 사항 + +가상현실 앱을 three.js로 만드는 것은 매우 기본적으로 three.js에게 WedXR을 사용할 것이라 알리기만 하면됩니다. WedXR에 몇가지 사항을 명확하게 해야 하는데 이를 생각하여 보면 먼저 카메라가 가리키는 방향은 향하고 있는지 VR시스템에서 제공해야합니다. 사용자가 머리를 돌려 보는 방향을 선택하기 때문입니다. 비슷하게 각 시스템 이후 VR 시스템에서 시야와 화면비가 제공됩니다. (각 시스템은 시야와 디스플레이 측면이 다릅니다) + +[반응형 웹페이지 만들기](threejs-responsive.html) 예시를 통하여 VR을 지원하도록 만들어 보겠습니다. + +시작하기 전에 안드로이드 스마트폰, 구글 데이드림, 오큘러스 고, 오큘러스 리프트, 바이브, +삼성 기어 VR, [WebXR browser](https://apps.apple.com/us/app/webxr-viewer/id1295998056)가 설치된 아이폰과 같은 VR 지원 장치가 필요합니다. + +다음으로, 로컬에서 실행 중인 경우 다음과 같은 간단한 웹 서버를 실행해야 합니다. +[the article on setting up](threejs-setup.html) 참조. + +VR을 보는 데 사용하는 장치가 실행 중인 컴퓨터와 다른 경우 +https를 통해 웹 페이지를 서비스해야 합니다. 그렇지 않으면 브라우저에서 사용을 허용하지 않습니다. +WebXR API. [the article on setting up](threejs-setup.html)에 언급된 서버 +[Servez](https://greggman.github.io/servez)에는 https를 사용할 수 있는 옵션이 있습니다. +확인 후 서버를 시작합니다. + +
+ +URL을 기록해 두십시오. 컴퓨터의 로컬 IP 주소가 필요합니다. 일반적으로 `192`, `172` 또는 `10`으로 시작합니다. `https://` 부분을 포함한 전체 주소를 입력하시면 됩니다. + +VR 기기의 브라우저로 이동합니다. NOTE: 작업 컴퓨터와 VR 장치는 동일한 로컬 네트워크에 있어야 합니다. +또는 WiFi이고 아마도 홈 네트워크에 있어야 할것입니다. NOTE: 많은 카페에서 이러한 방법으로 기계 대 기계 연결을 합니다. + + +아래와 같은 오류 메시지가 표시됩니다. "고급(advanced)"을 클릭한 다음 *진행(proceed)* 을 클릭합니다. + + + +이제 예제를 실행할 수 있습니다. + +실제로 WebVR 개발을 하려는 경우 배워야 할 또 다른 사항은 +[원격 디버깅(remote debugging)](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/) 입니다. +이를 통해 콘솔 경보, 오류, 실제로 [코드 디버그(debug your code)](threejs-debugging-javascript.html)가 가능합니다. + +아래 코드가 작동하는 것을 보고 싶다면 이 사이트에서 코드를 실행할 수 있습니다. + +가장 먼저 해야 할 일은 three.js를 포함시킨 후 VR 지원을 포함하는 것입니다. + +```js +import * as THREE from './resources/three/r132/build/three.module.js'; ++import {VRButton} from './resources/threejs/r132/examples/jsm/webxr/VRButton.js'; +``` + +이후 three.js's WebXR 지원을 활성화하며, 이를 위한 VR button을 페이지에 추가하여 주어야합니다. + +```js +function main() { + const canvas = document.querySelector('#c'); + const renderer = new THREE.WebGLRenderer({canvas}); ++ renderer.xr.enabled = true; ++ document.body.appendChild(VRButton.createButton(renderer)); +``` + +three.js가 렌더 루프를 실행하도록 해야 합니다. 지금까지 우리는 +`requestAnimationFrame`loop 를 사용하였습니다. 하지만 VR을 지원하기 위해서 우리는 three.js가 우리의 render loop를 관리할 수 있도록 해야합니다. 이 과정을 +`WebGLRenderer.setAnimationLoop`를 호출 및 루프를 호출하는 함수를 전달하여 진행 가능하게 합니다. + +```js +function render(time) { + time *= 0.001; + + if (resizeRendererToDisplaySize(renderer)) { + const canvas = renderer.domElement; + camera.aspect = canvas.clientWidth / canvas.clientHeight; + camera.updateProjectionMatrix(); + } + + cubes.forEach((cube, ndx) => { + const speed = 1 + ndx * .1; + const rot = time * speed; + cube.rotation.x = rot; + cube.rotation.y = rot; + }); + + renderer.render(scene, camera); + +- requestAnimationFrame(render); +} + +-requestAnimationFrame(render); ++renderer.setAnimationLoop(render); +``` + +세부 사항이 하나 더 있습니다. 우리는 아마 카메라 높이를 설정해야 합니다. +예로 서있는 사용자의 평균 키를 들 수 있습니다. + +```js +const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); ++camera.position.set(0, 1.6, 0); +``` + +큐브를 카메라 앞으로 이동합니다. + +```js +const cube = new THREE.Mesh(geometry, material); +scene.add(cube); + +cube.position.x = x; ++cube.position.y = 1.6; ++cube.position.z = -2; +``` + +카메라가 `z = 0` 에 있고 카메라는 -z축을 바라보기 때문에 우리는 `z = -2`에 배치 시켰습니다(cude). + + +이것은 매우 중요한 점을 제시합니다. **Units in VR are in meters**. +다른 말로 **One Unit = One Meter**. 이것은 카메라가 0보다 1.6미터 위에 있음을 의미합니다. +큐브의 중심은 카메라 앞에서 2미터입니다. +각 큐브 1x1x1미터 크기입니다. +이것은 VR이 *실제 세계* 에 사용자에 반응하여 사물을 조정해야 하기 때문에 중요합니다. 즉 사용자의 움직임과 VR에서의 움직임을 매치 시켜줄 단위가 필요합니다. + +그리고 그것으로(단위) 우리는 앞으로 3개의 회전하는 큐브와 VR에 들어갈 버튼 을 카메라 앞에 가져와야 합니다. + +{{{example url="../threejs-webvr-basic.html" }}} + + +카메라 주변에 방(참조할 수 있는 공간)과 같은 감싸는 것이 있으면 VR이 더 잘 작동합니다. 따라서 과거 [the article on backgrounds](threejs-backgrounds.html)에서 진행한 것과 유사하게 간단한 그리드 큐브맵을 추가해 보겠습니다. + +우리는 같은 그리드를 사용할 것입니다. +그리드 룸으로 제공할 큐브의 각 면에 대한 텍스처입니다. + +```js +const scene = new THREE.Scene(); ++{ ++ const loader = new THREE.CubeTextureLoader(); ++ const texture = loader.load([ ++ 'resources/images/grid-1024.png', ++ 'resources/images/grid-1024.png', ++ 'resources/images/grid-1024.png', ++ 'resources/images/grid-1024.png', ++ 'resources/images/grid-1024.png', ++ 'resources/images/grid-1024.png', ++ ]); ++ scene.background = texture; ++} +``` + +더욱 나아진것이 확인 가능합니다. + +{{{example url="../threejs-webvr-basic-w-background.html" }}} + +Note: VR을 실제로 보려면 WebXR 호환 장치가 필요합니다. +대부분의 Android 휴대폰은 Chrome 또는 Firefox를 사용하여 WebXR을 지원 합니다. +iOS의 경우 [WebXR App](https://apps.apple.com/us/app/webxr-viewer/id1295998056)를 참조 하시면 됩니다. +iOS에서 일반적으로 WebXR 지원은 2019년 5월 현재 지원되지 않습니다. + + +Android 또는 iPhone에서 WebXR을 사용하려면 *VR 헤드셋*이 필요합니다. +전화용. 골판지로 만든 1개에 5달러부터 어디에서나 구입할 수 있습니다. +불행히도 어떤 제품을 추천해야 할지 모르겠습니다. +저는 여러가지 제품을 구입하였습니다. 그 중 6개는 수년에 걸쳐 생산되었으며 모두 품질이 다릅니다. +$25 이상을 지불한 적은 없습니다. + +VR 헤드셋의 몇 가지 문제만 언급하겠습니다. + +1. 휴대전화에 맞습니까? + + 전화기는 다양한 크기로 제공되므로 VR 헤드셋이 일치해야 합니다. + 많은 헤드셋이 다양한 크기와 일치한다고 주장합니다. 내 경험 + 더 많은 스마트폰 크기와 일치할수록 기능은 나빠집니다. + 특정 크기에 맞게 설계된 잘 타협한 제품을 구매해야 합니다. + +2. 당신의 얼굴에 집중할 수 있습니까? + + 일부 장치에는 다른 장치보다 더 많은 조절 요소가 있습니다. 일반적으로는 + 렌즈가 눈에서 얼마나 멀리 떨어져 있는지, 그리고 렌즈가 얼마나 멀리 떨어져 있는지 + 최대 2개의 조절 요소가 존재합니다. + +3. 너무 반사적인가요? + + 당신의 눈에서 전화까지 플라스틱 원뿔의 많은 헤드셋. + 플라스틱이 반짝이거나 반사되면 다음과 같이 작동합니다. + 화면을 반사하는 거울처럼 매우 산만합니다. + + 리뷰 중 이 문제를 다루는 것으로 보이는 경우는 거의 없습니다. + +4. 당신의 얼굴에 편안한가요? + + 대부분의 장치는 안경처럼 코에 닿습니다. + 몇 분 후에 아플 수 있습니다. 일부는 주변에 스트랩이 있습니다. + 너의 머리 위로 가는 3번째 끈이 있습니다. 이것들은 + 장치를 올바른 위치에 유지하는 데 도움이 될 수도 있고 도움이 되지 않을 수도 있습니다. + + 대부분의 (모든?) 장치에서 눈이 중앙에 있어야 합니다. + 렌즈와 함께. 렌즈가 자신보다 약간 높거나 낮은 경우 + 눈 이미지의 초점이 흐려집니다. 이것은 매우 실망 스러울 수 있습니다. + 작업이 초점에 맞게 시작될 수 있지만 45-60초 후에 장치를 + 1mm 위 또는 아래로 이동하면 갑자기 + 흐릿한 이미지에 초점을 맞추려고 애쓰는 경우가 있습니다. + +5. 안경을 착용하고 사용할수 있습니까? + + 안경을 쓰신 분들은 리뷰를 읽어보시고 + 특정 헤드셋이 안경과 잘 매치되는 경우는 + 정말 아쉽게도 추천을 해드릴 수가 없습니다. + + [구글은 일부 판지로 만든 저렴한 추천](https://vr.google.com/cardboard/get-cardboard/) + 그들 중 일부는 $ 5만큼 낮으므로 거기에서 시작하고 즐길 수 있습니다. + 그런 다음 업그레이드를 고려하시면 됩니다. ($5는 커피 1잔 가격과 같으니 꼭 이용해 보세요!) + +또한 3가지 기본 유형의 장치가 있습니다. + +1. 3자유도(3dof), 입력 장치 없음 + + 이것은 일반적으로 전화 스타일이지만 때로는 할 수 있습니다. + 타사 입력 장치를 구입하십시오. 3 자유도 + 위/아래(1), 왼쪽/오른쪽(2)을 보고 기울일 수 있음을 의미합니다. + 머리를 좌우로(3). + +2. 1개의 입력 장치(3dof)로 3자유도(3dof) + + 이것은 기본적으로 Google Daydream과 Oculus GO입니다. + + 이것들은 또한 3개의 자유도를 허용하고 작은 + VR 내부에서 레이저 포인터처럼 작동하는 컨트롤러입니다. + 레이저 포인터의 자유도는 3개뿐입니다. + NS 시스템은 입력 장치가 가리키는 방향을 알 수 있지만 + 장치가 어디에 있는지 알 수 없습니다. + +3. 입력 장치(6dof)가 있는 6자유도(6dof) + + 이것들은 *좋은 물건*입니다. 6 자유도 + 이 장치는 사용자가 보고 있는 방향을 알 뿐만 아니라 + 그러나 그들은 또한 당신의 머리가 실제로 어디에 있는지 알고 있습니다. 그 의미는 + 왼쪽에서 오른쪽으로 또는 앞뒤로 움직이거나 일어서거나 앉는 경우 + 장치는 이것을 등록할 수 있고 VR의 모든 것은 그에 따라 움직입니다. + 놀랍도록 실제적인 느낌입니다. 당신도 이와 비슷한 경험을 하게될것입니다. + 아니면 적어도 저는 그랬고 지금도 그렇습니다. + + 또한 이러한 장치에는 일반적으로 2개의 컨트롤러가 포함됩니다. + 각 손에 대하여 시스템은 사용자의 위치를 정확히 알 수 있습니다. + (손이 어떤 방향으로 향하고 있는지 손을 뻗고, 만지고, 밀기, 비틀기 등...) + + +이 모든 것을 다루었지만 어떤 장치가 WebXR과 함께 작동할지 확신할 수 없습니다. +Chrome을 실행할 때 대부분의 Android 휴대전화가 작동할 것이라고 99% 확신합니다. +당신은 [`about:flags`](about:flags)에서 WebXR support를 켜야 할수도 있습니다. + +Google Draydream 또한 비슷하게 작동할 것입니다. +[`about:flags`](about:flags)에서 WebXR support 를 활성화 하세요. + +Oculus Rift, Vive 및 Vive Pro는 Cheome 이나 Firefox를 통해 작동합니다. + +Oculus Go와 Oculus Quest는 둘 다 사용자 정의 OS를 사용하지만 인터넷에 따르면 둘 다 작동하는 것으로 보입니다. + + + +이제, VR 장치와 WebXR에 대한 긴 설명에 마지막을 설명하겠습니다. + +* VR 및 Non-VR 모두 지원 + +내가 아는한 적어도 r112에서는 three.js 에 VR과 Non-VR 모드 모두를 지원하는 쉬운 방법이 없습니다. + +이상적으로 +VR 모드가 아니면 `OrbitControls`와 같은 것들을 사용하여 카메라를 제어할 수 있습니다. +그리고 이 전환 과정에서 일조의 여러 이벤트가 발생합니다. +VR 모드에서 벗어나 컨트롤을 켜고 끌 수 있습니다. + +만약 three.js가 앞서 말한 두가지를 지원해준다면 해당 artical을 업데이트 하도록 노력하겠습니다. +그때까지는 2가지 버전의 사이트를 만들거나 다음과 같이 URL에 flag 를 pass in 해줍니다. + + +``` +https://mysite.com/mycooldemo?allowvr=true +``` + +그럼 우리는 스위치 모드에 몇몇 링크를 추가할 수 있습니다. + +```html + + ++