{"id":12045,"date":"2026-01-07T09:00:44","date_gmt":"2026-01-07T00:00:44","guid":{"rendered":"http:\/\/www.gisdeveloper.co.kr\/?p=12045"},"modified":"2026-04-07T11:56:25","modified_gmt":"2026-04-07T02:56:25","slug":"three-js-%ed%80%b5-%eb%a0%88%ed%8d%bc%eb%9f%b0%ec%8a%a4-%ec%bd%94%eb%93%9c","status":"publish","type":"post","link":"http:\/\/www.gisdeveloper.co.kr\/?p=12045","title":{"rendered":"THREE.JS \ud035 \ub808\ud37c\ub7f0\uc2a4 \ucf54\ub4dc"},"content":{"rendered":"<p>Three.js\ub97c \uc774\uc6a9\ud55c \uac1c\ubc1c \uc2dc \uac1c\uc778\uc801\uc73c\ub85c \ube60\ub974\uac8c \ucc38\uc870\ud558\uae30 \uc704\ud574 \uc791\uc131\ud55c \uae00\uc785\ub2c8\ub2e4. <\/p>\n<h4>three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGL)<\/h4>\n<p><code>git clone https:\/\/github.com\/GISDEVCODE\/threejs-with-javascript-starter.git .<\/code><\/p>\n<h4>three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGPU)<\/h4>\n<p><code>git clone https:\/\/github.com\/GISDEVCODE\/threejs-webgpu-with-javascript-starter.git .<\/code><\/p>\n<h4>three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGPU + Typescript)<\/h4>\n<p><code>git clone https:\/\/github.com\/GISDEVCODE\/threejs-webgpu-with-typescript-starter.git .<\/code><\/p>\n<h4>R3F \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (Javascript)<\/h4>\n<p><code>git clone https:\/\/github.com\/GISDEVCODE\/r3f-with-javascript-starter.git .<\/code><\/p>\n<h4>Shader \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (Javascript)<\/h4>\n<p><code>git clone https:\/\/github.com\/GISDEVCODE\/shader-with-threejs-javascript-starter .<\/code><\/p>\n<h4>\ud328\ud0a4\uc9c0 \ucd5c\uc2e0 \ubc84\uc804\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8<\/h4>\n<p><code>npx npm-check-updates -u<\/code><\/p>\n<h4>\uadf8\ub9bc\uc790 \uc801\uc6a9\uc5d0 \ub300\ud55c \ucf54\ub4dc<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nrenderer.shadowMap.enabled = true;\r\nrenderer.shadowMap.type = THREE.VSMShadowMap;\r\n\r\nconst shadowLight = new THREE.DirectionalLight(0xffe79d, 0.7);\r\nshadowLight.position.set(150, 220, 100);\r\nshadowLight.target.position.set(0,0,0);\r\nshadowLight.castShadow = true;\r\nshadowLight.shadow.mapSize.width = 1024*10;\r\nshadowLight.shadow.mapSize.height = 1024*10;\r\nshadowLight.shadow.camera.top = shadowLight.shadow.camera.right = 1000;\r\nshadowLight.shadow.camera.bottom = shadowLight.shadow.camera.left = -1000;\r\nshadowLight.shadow.camera.far = 800;\r\nshadowLight.shadow.radius = 5;\r\nshadowLight.shadow.blurSamples = 5;\r\nshadowLight.shadow.bias = -0.0002;\r\n\r\nconst cameraHelper = new THREE.CameraHelper(shadowLight.shadow.camera);\r\nthis._scene.add(cameraHelper);\r\n\r\nisland.receiveShadow = true;\r\nisland.castShadow = true;\r\n<\/pre>\n<h4>\uc9c0\uc624\uba54\ud2b8\ub9ac\uc758 \uc88c\ud45c \uc218\uc815<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst sphereGeom = new THREE.SphereGeometry(6 + Math.floor(Math.random() * 12), 8, 8);\r\nconst sphereGeomPosition = sphereGeom.attributes.position;\r\nfor (var i = 0; i &lt; sphereGeomPosition.count; i++) {\r\n    sphereGeomPosition.setY(i, sphereGeomPosition.getY(i) + Math.random() * 4 - 2);\r\n    sphereGeomPosition.setX(i, sphereGeomPosition.getX(i) + Math.random() * 3 - 1.5);\r\n    sphereGeomPosition.setZ(i, sphereGeomPosition.getZ(i) + Math.random() * 3 - 1.5);\r\n}\r\n\r\nsphereGeom.computeVertexNormals();\r\nsphereGeom.attributes.position.needsUpdate = true;\r\n<\/pre>\n<h4>\uc9c0\uc624\uba54\ud2b8\ub9ac\uc5d0 \uc0ac\uc6a9\uc790 \uc815\uc758 \ub370\uc774\ud130 \uc8fc\uc785<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n\/\/ \uc8fc\uc785\r\nconst waves = [];\r\nconst waterGeoPositions = waterGeo.attributes.position;\r\nfor (let i = 0; i &lt; waterGeoPositions.count; i++) {\r\n    waves[i] = Math.random() * 100;\r\n}\r\nwaterGeo.setAttribute(\"wave\", new THREE.Float32BufferAttribute(waves, 1));\r\n\r\n\/\/ \uc77d\uae30\r\nconst waves = sea.geometry.attributes.wave;\r\n\r\nfor(let i=0; i&lt;positions.count; i++) {\r\n    const v = waves.getX(i);\r\n}\r\n<\/pre>\n<h4>\uc548\uac1c \uc124\uc815 \ucf54\ub4dc<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nscene.fog = new THREE.Fog(\"rgba(54,219,214,1)\", 1000, 1400);\r\n<\/pre>\n<h4>OrbitControls \uad00\ub828 \ucf54\ub4dc<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst controls = new OrbitControls(this._camera, this._divContainer);\r\ncontrols.minPolarAngle = -Math.PI \/ 2;\r\ncontrols.maxPolarAngle = Math.PI \/ 2 + 0.1;\r\ncontrols.enableZoom = true;\r\ncontrols.enablePan = false;\r\ncontrols.autoRotate = true;\r\ncontrols.autoRotateSpeed = 0.2;\r\n\r\nthis._controls = controls;\r\n\r\nthis._controls.update();\r\n<\/pre>\n<h4>Object3D\uc758 MBR \uc5bb\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst board = this._scene.getObjectByName(\"Board\");\r\nconst box = new THREE.Box3().setFromObject(board);\r\nconsole.log(box);\r\n<\/pre>\n<h4>Mesh\uc758 \uc6d4\ub4dc\uc88c\ud45c\uc5d0 \ub300\ud55c position \uc5bb\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nmesh.updateMatrixWorld();\r\n\r\nconst worldPos = new THREE.Vector3();\r\nworldPos.setFromMatrixPosition(mesh.matrixWorld);\r\n<\/pre>\n<h4>Faked Shadow<\/h4>\n<p>\uadf8\ub9bc\uc790\ub97c \uc704\ud55c \ub9e4\uc2dc\uc5d0 \ub300\ud55c \uc7ac\uc9c8 \uc18d\uc131 \uc9c0\uc815\uc774 \ud575\uc2ec. \ucc38\uace0\ub85c shadow\uc5d0 \ub300\ud55c \uc774\ubbf8\uc9c0\ub294 \ud22c\uba85 \uc774\ubbf8\uc9c0\uac00 \uc544\ub2d8. \uc989, \ubc30\uacbd\uc0c9\uc774 \ud558\uc580\uc0c9\uc778 \uc774\ubbf8\uc9c0\uc784.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst shadow = new THREE.TextureLoader().load( 'models\/gltf\/ferrari_ao.png' );\r\n\r\nconst mesh = new THREE.Mesh(\r\n    new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),\r\n    new THREE.MeshBasicMaterial( {\r\n        map: shadow, \r\n        blending: THREE.MultiplyBlending, \r\n        toneMapped: false, \r\n        transparent: true\r\n    } )\r\n);\r\nmesh.rotation.x = - Math.PI \/ 2;\r\nmesh.renderOrder = 2;\r\ncarModel.add( mesh );\r\n<\/pre>\n<h4>\ud14d\uc2a4\uccd0 \uc774\ubbf8\uc9c0 \ud488\uc9c8 \uc62c\ub9ac\uae30<\/h4>\n<p>\uc0d8\ud50c\ub9c1 \ud69f\uc218\ub97c \uc62c\ub9ac\ub294 \uac83\uc73c\ub85c \uc18d\ub3c4\ub294 \ub290\ub824\uc9c8 \uc218 \uc788\uc73c\ub098 \ud488\uc9c8\uc740 \ud5a5\uc0c1\ub428<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\ntexture.anisotropy = renderer.capabilities.getMaxAnisotropy();\r\n<\/pre>\n<h4>async \ub9ac\uc18c\uc2a4 \ub85c\ub529<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nasync function init() {\r\n    const rgbeLoader = new RGBELoader().setPath('textures\/equirectangular\/');\r\n    const gltfLoader = new GLTFLoader().setPath('models\/gltf\/DamagedHelmet\/glTF\/');\r\n\r\n    const [texture, gltf] = await Promise.all([\r\n        rgbeLoader.loadAsync( 'venice_sunset_1k.hdr' ),\r\n        gltfLoader.loadAsync( 'DamagedHelmet.gltf' ),\r\n    ]);\r\n}\r\n\r\ninit().catch(function(err) {\r\n    console.error(err);\r\n});\r\n<\/pre>\n<h4>\ud14d\uc2a4\uccd0\ub97c Canvas\ub85c \ud6c4\ub2e4\ub2e5 \ub9cc\ub4e4\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst canvas = document.createElement( 'canvas' );\r\ncanvas.width = 1;\r\ncanvas.height = 32;\r\n\r\nconst context = canvas.getContext( '2d' );\r\nconst gradient = context.createLinearGradient( 0, 0, 0, 32 );\r\ngradient.addColorStop( 0.0, '#ff0000' );\r\ngradient.addColorStop( 0.5, '#00ff00' );\r\ngradient.addColorStop( 1.0, '#0000ff' );\r\ncontext.fillStyle = gradient;\r\ncontext.fillRect( 0, 0, 1, 32 );\r\n\r\nconst sky = new THREE.Mesh(\r\n\tnew THREE.SphereGeometry( 10 ),\r\n\tnew THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } )\r\n);\r\nscene.add( sky );\r\n<\/pre>\n<h4>GLTF \ud30c\uc77c \ub85c\ub529<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nimport { GLTFLoader } from \"..\/examples\/jsm\/loaders\/GLTFLoader.js\"\r\n\r\nconst loader = new GLTFLoader();\r\nloader.load(\".\/data\/ring.glb\", gltf => {\r\n    const object = gltf.scene;\r\n    this._scene.add(object);\r\n});    \r\n<\/pre>\n<h4>InstancedMesh<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst mesh = new THREE.InstancedMesh(geometry, material, 10000)\r\n\r\nconst matrix = new THREE.Matrix4()\r\nconst dummy = new THREE.Object3D()\r\n \r\nfor(let i = 0; i < 10000; i++) {\r\n  mesh.getMatrixAt(i, matrix)\r\n  matrix.decompose(dummy.position, dummy.rotation, dummy.scale)\r\n  \r\n  dummy.rotation.x = Math.random()\r\n  dummy.rotation.y = Math.random()\r\n  dummy.rotation.z = Math.random()\r\n\r\n  dummy.updateMatrix()\r\n\r\n  mesh.setMatrixAt(i, dummy.matrix)\r\n  mesh.setColorAt(i, new THREE.Color(Math.random() * 0xffffff)\r\n}\r\n\r\nmesh.instanceMatrix.needsUpdate()\r\n<\/pre>\n<h4>Image \uae30\ubc18 \uad11\uc6d0(IBL)<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nimport { RGBELoader } from 'three\/examples\/jsm\/Addons.js'\r\n\r\n...\r\n\r\nnew RGBELoader().setPath(\".\/\").load(\"pine_attic_2k.hdr\", (data) => {\r\n  data.mapping = THREE.EquirectangularReflectionMapping;\r\n  \/\/ this.scene.background = data;\r\n  \/\/ this.scene.backgroundBlurriness = 0.6;\r\n  this.scene.environment = data;\r\n})\r\n<\/pre>\n<h4>GLTF \/ GLB \ud30c\uc77c \ub85c\ub529<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nimport { GLTFLoader, OrbitControls } from \"three\/addons\/Addons.js\"\r\n\r\n...\r\n\r\nconst loader = new GLTFLoader();\r\nloader.load(\r\n  \"fileName.glb\",\r\n  (gltf) => {\r\n    this._scene.add( gltf.scene );    \r\n  },\r\n  (xhr) => { console.log( ( xhr.loaded \/ xhr.total * 100 ) + \"% loaded\" ); },\r\n  (error) => { console.log( \"An error happened\" ); }\r\n);\r\n<\/pre>\n<h4>Object3D\ub97c \ube4c\ubcf4\ub4dc\ub85c \ub9cc\ub4e4\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nthis._mesh.quaternion.copy(this._camera.quaternion );\r\n\/\/ or\r\nthis._mesh.rotation.setFromRotationMatrix( this._camera.matrix );\r\n<\/pre>\n<h4>FPS \uce21\uc815<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nlet frameCount = 0;\r\nlet lastTime = performance.now();\r\nlet fps = 60;\r\n\r\nfunction updateFPS() { \/\/ \ub80c\ub354\ub9c1 \ud568\uc218\uc5d0\uc11c \ud638\ucd9c\ud574\uc57c \ud568\r\n  frameCount++;\r\n  const currentTime = performance.now();\r\n  const deltaTime = currentTime - lastTime;\r\n\r\n  if (deltaTime >= 1000) {\r\n    fps = Math.round((frameCount * 1000) \/ deltaTime);\r\n    frameCount = 0;\r\n    lastTime = currentTime;\r\n  }\r\n}\r\n<\/pre>\n<h4>FPS \uc81c\ud55c\ud558\uae30<\/h4>\n<p>requestAnimationFrame\ub85c \ub80c\ub354\ub9c1\uc744 \uc218\ud589\ud558\uba74 \ucd5c\ub300\ud55c \ub9ce\uc740 \ud504\ub808\uc784\uc744 \uc0dd\uc131\ud558\uae30 \ub428. \uc544\ub798\ub294 \uc6d0\ud558\ub294 \ud504\ub808\uc784\uc218\ub85c \uc81c\ud55c\ud558\uae30 \uc704\ud574 \ub2e4\uc74c \ucf54\ub4dc\ub85c 30 \ud504\ub808\uc784 \uc81c\ud55c\uc785\ub2c8\ub2e4.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n  _elapsedTime = 0;\r\n  _fps = 1 \/ 60\r\n  render() {\r\n    const delta = this._clock.getDelta();\r\n    this.update(delta);\r\n    this._elapsedTime += delta;\r\n    \r\n    if (this._elapsedTime >= (this._fps)) {\r\n      this._stats.begin();\r\n      this._renderer.render(this._scene, this._camera);\r\n      this._stats.end();\r\n      this._elapsedTime %= this._fps;\r\n    }\r\n\r\n    requestAnimationFrame(this.render.bind(this));\r\n  }\r\n<\/pre>\n<h4>\ud654\uba74 \uc88c\ud45c\ub97c \uc6d4\ub4dc \uc88c\ud45c\ub85c \ubcc0\ud658\ud558\ub294 \ud568\uc218<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n\/\/ \uc2a4\ud06c\ub9b0 \uc88c\ud45c -> \uc6d4\ub4dc \uc88c\ud45c \ubcc0\ud658 \ud568\uc218\r\nscreenToWorld(screenX, screenY, ndcZ = 0.5) { \/\/ ndcZ=0.5\ub85c \uc124\uc815 (\uce74\uba54\ub77c \ubc29\ud5a5)\r\n  \/\/ 1. \ud654\uba74 \ud53d\uc140 \uc88c\ud45c\ub97c \uc815\uaddc\ud654\ub41c \ub514\ubc14\uc774\uc2a4 \uc88c\ud45c(NDC)\ub85c \ubcc0\ud658\r\n  const ndcX = (screenX \/ this._divContainer.clientWidth) * 2 - 1;\r\n  const ndcY = -(screenY \/ this._divContainer.clientHeight) * 2 + 1;\r\n\r\n  \/\/ 2. NDC\ub97c 3D \uacf5\uac04\uc758 Ray\ub85c \ubcc0\ud658\r\n  const vector = new THREE.Vector3(ndcX, ndcY, ndcZ);\r\n  vector.unproject(this._camera);\r\n\r\n  \/\/ 3. Ray\ub97c \ub530\ub77c \uc6d4\ub4dc \uc88c\ud45c\ub97c \uacc4\uc0b0\r\n  return vector;\r\n}\r\n\r\nscreenToWorldAtZ(screenX, screenY, targetZ = 0) {\r\n  \/\/ 1. \ud654\uba74 \ud53d\uc140 \uc88c\ud45c -> NDC \ubcc0\ud658\r\n  const ndcX = (screenX \/ this._divContainer.clientWidth) * 2 - 1;\r\n  const ndcY = -(screenY \/ this._divContainer.clientHeight) * 2 + 1;\r\n\r\n  \/\/ 2. NDC -> Ray \uc0dd\uc131\r\n  const raycaster = new THREE.Raycaster();\r\n  const vector = new THREE.Vector2(ndcX, ndcY);\r\n  raycaster.setFromCamera(vector, this._camera);\r\n\r\n  \/\/ 3. Ray\uc640 z=targetZ \ud3c9\uba74\uc758 \uad50\ucc28\uc810 \uacc4\uc0b0\r\n  const direction = raycaster.ray.direction;\r\n  const origin = raycaster.ray.origin;\r\n  const t = (targetZ - origin.z) \/ direction.z;\r\n\r\n  const worldPoint = origin.clone().add(direction.multiplyScalar(t));\r\n  return worldPoint;\r\n}\r\n<\/pre>\n<h4>\ubaa8\ub378\uc744 \ud654\uba74 \uc911\uc2ec\uc5d0 \ud45c\uc2dc\ud558\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n_zoomFix(model, distanceFactor = 1.5) {\r\n  const box = new THREE.Box3().setFromObject(model);\r\n  const center = box.getCenter(new THREE.Vector3());\r\n\r\n  model.position.sub(center);\r\n\r\n  const size = box.getSize(new THREE.Vector3());\r\n  const maxDimension = Math.max(size.x, size.y, size.z);\r\n  const cameraDistance = maxDimension * distanceFactor;\r\n  this._camera.position.set(0, 0, cameraDistance);\r\n  this._camera.lookAt(this._scene.position);\r\n}\r\n<\/pre>\n<h4>\ubaa8\ub378\uc758 \ud2b9\uc815 \ud398\uc774\uc2a4\uc5d0 \ub300\ud55c \ubc95\uc120 \ubca1\ud130 \uad6c\ud558\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n_getNormal(mesh, faceIndex) {\r\n  const normalAttribute = mesh.geometry.getAttribute(\"normal\");\r\n  const index = mesh.geometry.index;\r\n  const faceStartIndex = faceIndex * 3;\r\n\r\n  const vertexIndexA = index.getX(faceStartIndex);\r\n  const vertexIndexB = index.getX(faceStartIndex + 1);\r\n  const vertexIndexC = index.getX(faceStartIndex + 2);\r\n\r\n  const normalA = new THREE.Vector3();\r\n  const normalB = new THREE.Vector3();\r\n  const normalC = new THREE.Vector3();\r\n  const faceNormal = new THREE.Vector3();\r\n\r\n  normalA.fromBufferAttribute(normalAttribute, vertexIndexA);\r\n  normalB.fromBufferAttribute(normalAttribute, vertexIndexB);\r\n  normalC.fromBufferAttribute(normalAttribute, vertexIndexC);\r\n\r\n  faceNormal.addVectors(normalA, normalB).add(normalC).divideScalar(3).normalize();\r\n\r\n  return faceNormal;\r\n}\r\n<\/pre>\n<h4>\uc5b4\ub5a4 \uc704\uce58\uc5d0\uc11c \ub9e4\uc2dc\uc5d0 \uac00\uc7a5 \uac00\uae4c\uc6b4 \ub9e4\uc2dc \ud45c\uba74\uc758 \uc704\uce58\ub97c \uc5bb\ub294 \ucf54\ub4dc<\/h4>\n<p>\uc544\ub798 \ud568\uc218\uc758 point \ud30c\ub77c\uba54\ud130\ub294 \uc5b4\ub5a4 \uc704\uce58\uc774\uace0 mesh \ud30c\ub77c\uba54\ud130\uac00 \ub300\uc0c1 \uba54\uc2dc\uc785\ub2c8\ub2e4. point\uc5d0\uc11c \uac00\uc7a5 \uac00\uae4c\uc6b4 \ub9e4\uc2dc \ud45c\uba74\uc758 \uc88c\ud45c\uac00 \ubcc0\ud658\ub429\ub2c8\ub2e4.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nfindClosestPointOnMesh(\/* Vector3 *\/ point, \/* Mesh *\/ mesh) {\r\n  const geometry = mesh.geometry;\r\n  const vertices = geometry.attributes.position;\r\n  const indices = geometry.index ? geometry.index.array : null;\r\n\r\n  let closestPoint = new THREE.Vector3();\r\n  let minDistance = Infinity;\r\n\r\n  const localPoint = mesh.worldToLocal(point);\r\n\r\n  if (indices) {\r\n    for (let i = 0; i < indices.length; i += 3) {\r\n      const a = new THREE.Vector3().fromBufferAttribute(vertices, indices[i]);\r\n      const b = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 1]);\r\n      const c = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 2]);\r\n\r\n      const tempPoint = new THREE.Vector3();\r\n      const triangle = new THREE.Triangle(a, b, c);\r\n      triangle.closestPointToPoint(localPoint, tempPoint);\r\n\r\n      const distance = localPoint.distanceTo(tempPoint);\r\n      if (distance < minDistance) {\r\n        minDistance = distance;\r\n        closestPoint.copy(tempPoint);\r\n      }\r\n    }\r\n  } else {\r\n    for (let i = 0; i < vertices.count; i += 3) {\r\n      const a = new THREE.Vector3().fromBufferAttribute(vertices, i);\r\n      const b = new THREE.Vector3().fromBufferAttribute(vertices, i + 1);\r\n      const c = new THREE.Vector3().fromBufferAttribute(vertices, i + 2);\r\n\r\n      const tempPoint = new THREE.Vector3();\r\n      const triangle = new THREE.Triangle(a, b, c);\r\n      triangle.closestPointToPoint(localPoint, tempPoint);\r\n\r\n      const distance = localPoint.distanceTo(tempPoint);\r\n      if (distance < minDistance) {\r\n        minDistance = distance;\r\n        closestPoint.copy(tempPoint);\r\n      }\r\n    }\r\n  }\r\n\r\n  return mesh.localToWorld(closestPoint);\r\n}\r\n<\/pre>\n<p>\uc544\ub798\ub294 \uc704\uc758 \ud568\uc218\uac00 \uc801\uc6a9\ub41c \uc2e4\ud589 \ud654\uba74\uc778\ub370, \ub178\ub780\uc0c9 \ud3ec\uc778\ud2b8 \uc704\uce58\uac00 \uc784\uc758\uc758 \uc704\uce58(point \ud30c\ub77c\uba54\ud130)\uc774\uace0 \ube68\uac04\uc0c9\uc774 \ud568\uc218\uc758 \uacb0\uacfc \uc88c\ud45c\uc785\ub2c8\ub2e4.<\/p>\n<p><center><div style=\"width: 525px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-12045-1\" width=\"525\" height=\"432\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"http:\/\/www.gisdeveloper.co.kr\/wp-content\/uploads\/2024\/12\/2024-12-25-23-01-39.mp4?_=1\" \/><a href=\"http:\/\/www.gisdeveloper.co.kr\/wp-content\/uploads\/2024\/12\/2024-12-25-23-01-39.mp4\">http:\/\/www.gisdeveloper.co.kr\/wp-content\/uploads\/2024\/12\/2024-12-25-23-01-39.mp4<\/a><\/video><\/div><\/center><\/p>\n<h4>DataTexture\ub97c \uc774\uc6a9\ud55c Raw \ub370\uc774\ud130\ub85c \ud14d\uc2a4\uccd0 \uc0dd\uc131<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\n    const width = 256;\r\n    const height = 256;\r\n    const size = width * height;\r\n    const data = new Uint8Array(4 * size); \/\/ RGBA \ub370\uc774\ud130\r\n\r\n    for (let i = 0; i < size; i++) {\r\n      const stride = i * 4;\r\n\r\n      data[stride] = Math.floor(Math.random() * 256);     \/\/ R\r\n      data[stride + 1] = Math.floor(Math.random() * 256); \/\/ G\r\n      data[stride + 2] = Math.floor(Math.random() * 256); \/\/ B\r\n      data[stride + 3] = 255;                             \/\/ A\r\n    }\r\n\r\n    const texture = new THREE.DataTexture(data, width, height);\r\n    texture.needsUpdate = true;\r\n\r\n    const material = new THREE.MeshBasicMaterial({ map: texture });\r\n    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material);\r\n    this._scene.add(mesh);\r\n<\/pre>\n<h4>\uc5ec\ub7ec \uac1c\uc758 \uc9c0\uc624\uba54\ud2b8\ub9ac\ub4e4\uc744 \ud55c \uac1c\uc758 \uc9c0\uc624\uba54\ud2b8\ub9ac\ub85c \ubcd1\ud569\ud558\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst mat3 = new THREE.MeshBasicMaterial();\r\nconst pumpkin = await this.loadGLTF('.\/resources\/models\/pumpkin.glb');\r\nconst geos = [];\r\n\r\n\/\/ \uc544\ub798\uc758 2\uc904\uc5d0 \ub300\ud55c \ubcc0\ud658 \ucf54\ub4dc\ub294 updateMatrixWorld \ub9e4\uc11c\ub4dc\uac00 \uc2e4\ud589\ub418\uba74 \ubcc0\ud658 \uac12\uc774 matrixWorld\uc5d0 \ubc18\uc601\ub428\r\npumpkin.children[0].position.set(0, 0, 0);\r\npumpkin.children[0].scale.setScalar(0.01);\r\n\r\npumpkin.children[0].traverse(child => {\r\n  child.updateMatrixWorld(); \r\n  if(child.isMesh) {\r\n    if(child.geometry) {\r\n      const attribute = child.geometry.getAttribute('position').clone();\r\n      const geometry = new THREE.BufferGeometry();\r\n      geometry.setIndex(child.geometry.index);\r\n      geometry.setAttribute('position', attribute);\r\n      geometry.applyMatrix4(child.matrixWorld);\r\n      geos.push(geometry);\r\n    }\r\n  }\r\n});\r\nconst combinedGeometry = BufferGeometryUtils.mergeGeometries(geos);\r\nconst combinedMesh = new THREE.Mesh(combinedGeometry, mat3);\r\n<\/pre>\n<h4>\ub9c8\uc6b0\uc2a4 \ucee4\uc11c \uc704\uce58\uc5d0 \ub300\ud55c 3\ucc28\uc6d0 \uacf5\uac04 \uc88c\ud45c(\ud3c9\uba74 \uae30\uc900) \uc5bb\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst mouse3D = new THREE.Vector3(0, 0, 0);\r\nconst raycaster = new THREE.Raycaster();\r\nconst intersectionPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\r\n\r\nwindow.addEventListener('mousemove', (event) => {\r\n  const mouse = new THREE.Vector2(\r\n    (event.clientX \/ window.innerWidth) * 2 - 1,\r\n    -(event.clientY \/ window.innerHeight) * 2 + 1\r\n  );\r\n  raycaster.setFromCamera(mouse, camera);\r\n\r\n  \/\/ Plane \uc218\ud559 \ubaa8\ub378\uc744 \ud1b5\ud55c \ub9c8\uc6b0\uc2a4 \ucee4\uc11c\uc758 Plane \uc0c1\uc758 3\ucc28\uc6d0 \uc704\uce58 \uc5bb\uae30(mouse3D\uc5d0 \uc800\uc7a5)\r\n  raycaster.ray.intersectPlane(intersectionPlane, mouse3D);\r\n});\r\n<\/pre>\n<h4>TSL \ub178\ub4dc\ud568\uc218\uc778 instancedArray\ub85c \uc0dd\uc131\ub41c GPU \uba54\ubaa8\ub9ac\ub97c CPU \uba54\ubaa8\ub9ac\uc758 \ubc30\uc5f4\ud615\ud0dc\ub85c \uac00\uc838\uc624\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nthis.velocityBuffer = instancedArray(this.COUNT, 'vec3');\r\n\r\n...\r\n\r\nconst resultBuffer = await renderer.getArrayBufferAsync(this.velocityBuffer.value);\r\nconst resultArray = new Float32Array(resultBuffer);\r\nconsole.log(resultArray)\r\n<\/pre>\n<h4>billboarding TSL<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nmaterial.vertexNode = billboarding({ horizontal: true, vertical: true });\r\n<\/pre>\n<h4>\ud3b8\ubbf8\ubd84\uc5d0 \uc758\ud55c \ub178\ub9d0 \ubca1\ud130 \uad6c\ud558\uae30<\/h4>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">\r\nconst normalVector = Fn(([pos]) => {\r\n  const dU = dFdx(pos).toConst(), dV = dFdy(pos).toConst();\r\n  return transformNormalToView(cross(dU, dV).normalize());\r\n  \/\/ return cross(dU, dV).normalize();\r\n});\r\n<\/pre>\n<p>normalVector\uc758 \uc778\uc790\uac12\uc73c\ub85c positionLocal \ub178\ub4dc\uac00 \uc62c \uc218 \uc788\ub2e4.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Three.js\ub97c \uc774\uc6a9\ud55c \uac1c\ubc1c \uc2dc \uac1c\uc778\uc801\uc73c\ub85c \ube60\ub974\uac8c \ucc38\uc870\ud558\uae30 \uc704\ud574 \uc791\uc131\ud55c \uae00\uc785\ub2c8\ub2e4. three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGL) git clone https:\/\/github.com\/GISDEVCODE\/threejs-with-javascript-starter.git . three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGPU) git clone https:\/\/github.com\/GISDEVCODE\/threejs-webgpu-with-javascript-starter.git . three.js \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (WebGPU + Typescript) git clone https:\/\/github.com\/GISDEVCODE\/threejs-webgpu-with-typescript-starter.git . R3F \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (Javascript) git clone https:\/\/github.com\/GISDEVCODE\/r3f-with-javascript-starter.git . Shader \uae30\ubcf8 \ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 (Javascript) git clone &hellip; <\/p>\n<p class=\"link-more\"><a href=\"http:\/\/www.gisdeveloper.co.kr\/?p=12045\" class=\"more-link\">\ub354 \ubcf4\uae30<span class=\"screen-reader-text\"> &#8220;THREE.JS \ud035 \ub808\ud37c\ub7f0\uc2a4 \ucf54\ub4dc&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[145,139,1],"tags":[],"class_list":["post-12045","post","type-post","status-publish","format-standard","hentry","category-three-js","category-webgl","category-uncategorized"],"_links":{"self":[{"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/posts\/12045","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12045"}],"version-history":[{"count":45,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/posts\/12045\/revisions"}],"predecessor-version":[{"id":16825,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=\/wp\/v2\/posts\/12045\/revisions\/16825"}],"wp:attachment":[{"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12045"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=12045"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.gisdeveloper.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=12045"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}