No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3d.js 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /*
  2. * 3d.js
  3. *
  4. * Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
  5. */
  6. import * as THREE from 'three';
  7. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  8. import { STLLoader } from 'three/addons/loaders/STLLoader.js'
  9. import { VRMLLoader } from 'three/addons/loaders/VRMLLoader.js';
  10. // https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
  11. function fitCameraToCenteredObject(camera, object, offset, orbitControls, yOffset) {
  12. const boundingBox = new THREE.Box3();
  13. boundingBox.setFromObject( object );
  14. var size = new THREE.Vector3();
  15. boundingBox.getSize(size);
  16. // figure out how to fit the box in the view:
  17. // 1. figure out horizontal FOV (on non-1.0 aspects)
  18. // 2. figure out distance from the object in X and Y planes
  19. // 3. select the max distance (to fit both sides in)
  20. //
  21. // The reason is as follows:
  22. //
  23. // Imagine a bounding box (BB) is centered at (0,0,0).
  24. // Camera has vertical FOV (camera.fov) and horizontal FOV
  25. // (camera.fov scaled by aspect, see fovh below)
  26. //
  27. // Therefore if you want to put the entire object into the field of view,
  28. // you have to compute the distance as: z/2 (half of Z size of the BB
  29. // protruding towards us) plus for both X and Y size of BB you have to
  30. // figure out the distance created by the appropriate FOV.
  31. //
  32. // The FOV is always a triangle:
  33. //
  34. // (size/2)
  35. // +--------+
  36. // | /
  37. // | /
  38. // | /
  39. // | F° /
  40. // | /
  41. // | /
  42. // | /
  43. // |/
  44. //
  45. // F° is half of respective FOV, so to compute the distance (the length
  46. // of the straight line) one has to: `size/2 / Math.tan(F)`.
  47. //
  48. // FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
  49. // the camera.fov is the vertical FOV.
  50. const fov = camera.fov * ( Math.PI / 180 );
  51. const fovh = 2*Math.atan(Math.tan(fov/2) * camera.aspect);
  52. let dx = size.z / 2 + Math.abs( size.x / 2 / Math.tan( fovh / 2 ) );
  53. let dy = size.z / 2 + Math.abs( size.y / 2 / Math.tan( fov / 2 ) );
  54. let cameraZ = Math.max(dx, dy);
  55. // offset the camera, if desired (to avoid filling the whole canvas)
  56. if( offset !== undefined && offset !== 0 ) cameraZ *= offset;
  57. camera.position.set( 0, yOffset * cameraZ, cameraZ );
  58. // set the far plane of the camera so that it easily encompasses the whole object
  59. const minZ = boundingBox.min.z;
  60. const cameraToFarEdge = ( minZ < 0 ) ? -minZ + cameraZ : cameraZ - minZ;
  61. camera.far = cameraToFarEdge * 3;
  62. camera.updateProjectionMatrix();
  63. if ( orbitControls !== undefined ) {
  64. // set camera to rotate around the center
  65. orbitControls.target = new THREE.Vector3(0, 0, 0);
  66. // prevent camera from zooming out far enough to create far plane cutoff
  67. orbitControls.maxDistance = cameraToFarEdge * 2;
  68. }
  69. }
  70. export function init_3d(path, container, status, div_width, div_height) {
  71. const width = div_width;
  72. const height = div_height;
  73. const scene = new THREE.Scene();
  74. scene.add(new THREE.AxesHelper(1));
  75. const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
  76. const renderer = new THREE.WebGLRenderer();
  77. renderer.setSize(width, height);
  78. const controls = new OrbitControls(camera, renderer.domElement);
  79. controls.enableDamping = true;
  80. controls.autoRotate = true;
  81. if (path.endsWith(".stl")) {
  82. const light_amb = new THREE.AmbientLight(0x424242);
  83. scene.add(light_amb);
  84. for (const i of [1, -1]) {
  85. for (const j of [0, 1, 2]) {
  86. const light = new THREE.DirectionalLight(0xffffff, 0.5);
  87. light.position.set(i * (j == 0 ? 1 : 0),
  88. i * (j == 1 ? 1 : 0),
  89. i * (j == 2 ? 1 : 0));
  90. scene.add(light);
  91. }
  92. }
  93. const material = new THREE.MeshStandardMaterial();
  94. //material.roughness = 0.75;
  95. const loader = new STLLoader();
  96. loader.load(
  97. path,
  98. function (geometry) {
  99. const mesh = new THREE.Mesh(geometry, material);
  100. scene.add(mesh);
  101. fitCameraToCenteredObject(camera, scene, 0, controls, 0);
  102. },
  103. (xhr) => {
  104. const s = (xhr.loaded / xhr.total) * 100 + '% loaded';
  105. console.log(s);
  106. status.textContent = s;
  107. },
  108. (error) => {
  109. console.log(error);
  110. status.textContent = error;
  111. }
  112. );
  113. } else if (path.endsWith(".wrl")) {
  114. const light_amb = new THREE.AmbientLight(0xffffff);
  115. scene.add(light_amb);
  116. const loader = new VRMLLoader();
  117. loader.load(
  118. path,
  119. function (object) {
  120. scene.add(object);
  121. fitCameraToCenteredObject(camera, scene, 0, controls, 0);
  122. },
  123. (xhr) => {
  124. const s = (xhr.loaded / xhr.total) * 100 + '% loaded';
  125. console.log(s);
  126. status.textContent = s;
  127. },
  128. (error) => {
  129. console.log(error);
  130. status.textContent = error;
  131. }
  132. );
  133. } else {
  134. const s = "error: unknown filetype for " + path;
  135. console.log(s);
  136. status.textContent = s;
  137. }
  138. camera.position.z = 50;
  139. controls.update();
  140. function animate() {
  141. requestAnimationFrame(animate);
  142. controls.update();
  143. renderer.render(scene, camera);
  144. }
  145. animate();
  146. status.textContent = "3D model ready!";
  147. container.appendChild(renderer.domElement);
  148. const div = document.createElement("div");
  149. div.style.position = "absolute";
  150. div.style.left = "5px";
  151. div.style.top = "5px";
  152. div.style.background = "white";
  153. div.style.color = "black";
  154. container.appendChild(div);
  155. const chk_ar = document.createElement("input");
  156. chk_ar.type = "checkbox";
  157. chk_ar.checked = true;
  158. chk_ar.addEventListener('change', function() {
  159. controls.autoRotate = this.checked;
  160. });
  161. const div_ar = document.createElement("div");
  162. div_ar.appendChild(chk_ar);
  163. div_ar.appendChild(document.createTextNode("Auto-Rotate"));
  164. div.appendChild(div_ar);
  165. const btn_rst = document.createElement("input");
  166. btn_rst.type = "button";
  167. btn_rst.value = "Reset Camera";
  168. btn_rst.addEventListener('click', function() {
  169. fitCameraToCenteredObject(camera, scene, 0, controls, 0);
  170. });
  171. const div_rst = document.createElement("div");
  172. div_rst.appendChild(btn_rst);
  173. div.appendChild(div_rst);
  174. }