NoSloppy

Untitled

Apr 3rd, 2026
119
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 30.91 KB | None | 0 0
  1. import * as THREE from 'three';
  2. import {VRButton} from 'three/addons/webxr/VRButton.js';
  3. import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
  4. import {RGBELoader} from "three/examples/jsm/loaders/RGBELoader";
  5. // import {PMREMGenerator} from "three/src/extras/PMREMGenerator";
  6. // var EquirectangularToCubemap = require( 'three.equirectangular-to-cubemap' );
  7. import { Vector3 } from 'three/src/math/Vector3.js';
  8. import { Vector2 } from 'three/src/math/Vector2.js';
  9. import { Float32BufferAttribute } from 'three/src/core/BufferAttribute.js';
  10. import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer"
  11. import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
  12. import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
  13. // import { AfterimagePass } from 'three/examples/jsm/postprocessing/AfterimagePass.js';
  14.  
  15. // let origin;
  16. // window.origin = origin;
  17.  
  18. const scene = new THREE.Scene();
  19. const camera = new THREE.PerspectiveCamera( 14, window.innerWidth / window.innerHeight, 0.1, 1000 );
  20. camera.position.set(0, 1.6, 0);
  21.  
  22. window.camera = camera;
  23.  
  24. window.fullscreenActive = false;
  25. let normalHeight = null;
  26. window.enlargeCanvas = false;
  27.  
  28. const pageLeft = document.querySelector('.page-left');
  29. var CANVAS = document.getElementById("canvas_id");
  30. const renderer = new THREE.WebGLRenderer( { canvas: CANVAS, antialias: true });
  31. renderer.xr.enabled = true;
  32. window.renderer = renderer;
  33.  
  34. //document.body.appendChild(VRButton.createButton(renderer));
  35. renderer.setSize( CANVAS.clientWidth, CANVAS.clientHeight );
  36. //document.body.appendChild( renderer.domElement );
  37.  
  38. // if (!origin) {
  39. // const geometry = new THREE.SphereGeometry(1.5, 16, 16); // or even 0.5 for small
  40. // const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
  41. // origin = new THREE.Mesh(geometry, material);
  42. // scene.add(origin);
  43. // }
  44.  
  45. let bgPlane = null;
  46. let bgMaterial = null;
  47. let bgUniforms = null;
  48.  
  49. function createBgPlane() {
  50. // depth of the plane (same as .position.z)
  51. const planeZ = -300;
  52.  
  53. // compute world-space height of the frustum at that depth
  54. const vFOV = THREE.MathUtils.degToRad( camera.fov ); // vertical FOV in radians
  55. const height = 2 * Math.tan( vFOV / 2 ) * Math.abs( planeZ ); // full frustum height
  56.  
  57. // compute width from aspect
  58. const width = height * camera.aspect;
  59.  
  60. // add some extra vertical overhang (here 50% taller than the view)
  61. const extra = 1.5;
  62.  
  63. if (bgPlane) {
  64. scene.remove(bgPlane);
  65. bgPlane.geometry.dispose();
  66. }
  67.  
  68. bgPlane = new THREE.Mesh(
  69. new THREE.PlaneGeometry( width, height * extra ),
  70. bgMaterial
  71. );
  72. bgPlane.position.set(0, 1.6, planeZ);
  73. scene.add(bgPlane);
  74. bgPlane.visible = (window.showBackground !== false);
  75. window.bgPlane = bgPlane;
  76. }
  77.  
  78. function getDesiredCanvasSize() {
  79. if (window.fullscreenActive) {
  80. // If we're fullscreen, base size on the actual fullscreen element
  81. const fullscreen = document.fullscreenElement || document.webkitFullscreenElement;
  82. let rect;
  83. if (fullscreen) {
  84. rect = fullscreen.getBoundingClientRect();
  85. } else {
  86. rect = { width: window.innerWidth, height: window.innerHeight };
  87. }
  88. return {
  89. width: Math.floor(rect.width),
  90. height: Math.floor(rect.height)
  91. };
  92. }
  93.  
  94. // Normal / Enlarge
  95. const width = pageLeft ? pageLeft.offsetWidth : window.innerWidth * 2 / 3;
  96. let height;
  97. if (enlargeCanvas) {
  98. height = window.innerHeight / 1.5;
  99. } else {
  100. height = window.innerHeight / 2.2;
  101. }
  102. return { width, height };
  103. }
  104.  
  105. function resizeCanvasAndCamera() {
  106. const { width, height } = getDesiredCanvasSize();
  107. renderer.setSize(width, height, true);
  108. camera.aspect = width / height;
  109.  
  110. // Use width for resizing, not height
  111. const visualDistance = 150; // manually tuned to fit nicely
  112. const fitFov = 2 * THREE.MathUtils.radToDeg(
  113. // Saber length in units = 100
  114. Math.atan((100.0 / camera.aspect) / (2 * visualDistance))
  115. );
  116. camera.fov = fitFov;
  117.  
  118. camera.updateProjectionMatrix();
  119. normalHeight = window.innerHeight / 3;
  120. }
  121. window.resizeCanvasAndCamera = resizeCanvasAndCamera;
  122.  
  123.  
  124. const bloom = false;
  125.  
  126. const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 4, 1, 2.5);
  127. const composer = new EffectComposer(renderer);
  128. const renderPass = new RenderPass(scene, camera);
  129.  
  130. if (bloom) {
  131. composer.addPass(renderPass);
  132. composer.addPass(bloomPass);
  133. }
  134.  
  135. var hilt;
  136. var blade;
  137.  
  138. THREE.ShaderChunk.tonemapping_pars_fragment = THREE.ShaderChunk.tonemapping_pars_fragment.replace(
  139. 'vec3 CustomToneMapping( vec3 color ) { return color; }',
  140. `vec3 CustomToneMapping(vec3 color) {
  141. color += vec3(dot(max(color - vec3(1), vec3(0)), vec3(0.33)));
  142. return color;
  143. }`
  144. );
  145.  
  146. renderer.toneMapping = THREE.CustomToneMapping;
  147.  
  148. class BladeGeometry extends THREE.BufferGeometry {
  149.  
  150. constructor( radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {
  151. super();
  152. this.type = 'CylinderGeometry';
  153. this.parameters = {
  154. radiusTop: radiusTop,
  155. radiusBottom: radiusBottom,
  156. height: height,
  157. radialSegments: radialSegments,
  158. heightSegments: heightSegments,
  159. openEnded: openEnded,
  160. thetaStart: thetaStart,
  161. thetaLength: thetaLength
  162. };
  163.  
  164. const scope = this;
  165.  
  166. radialSegments = Math.floor( radialSegments );
  167. heightSegments = Math.floor( heightSegments );
  168.  
  169. // buffers
  170. const indices = [];
  171. const vertices = [];
  172. const normals = [];
  173. const uvs = [];
  174.  
  175. // helper variables
  176. let index = 0;
  177. const indexArray = [];
  178. const halfHeight = height / 2;
  179. let groupStart = 0;
  180.  
  181. // generate geometry
  182. generateTorso();
  183.  
  184. if ( openEnded === false ) {
  185. if ( radiusTop > 0 ) generateCap( true );
  186. if ( radiusBottom > 0 ) generateCap( false );
  187. }
  188.  
  189. // build geometry
  190. this.setIndex( indices );
  191. this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
  192. this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
  193. this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
  194.  
  195. function generateTorso() {
  196.  
  197. const normal = new Vector3();
  198. const vertex = new Vector3();
  199.  
  200. let groupCount = 0;
  201.  
  202. // this will be used to calculate the normal
  203. const slope = ( radiusBottom - radiusTop ) / height;
  204.  
  205. // generate vertices, normals and uvs
  206. for ( let y = 0; y <= heightSegments; y ++ ) {
  207. const indexRow = [];
  208. const v = y / heightSegments;
  209.  
  210. // calculate the radius of the current row
  211. const radius = v * ( radiusBottom - radiusTop ) + radiusTop;
  212. for ( let x = 0; x < radialSegments; x ++ ) {
  213. const u = x / radialSegments;
  214. const theta = u * thetaLength + thetaStart;
  215. const sinTheta = Math.sin( theta );
  216. const cosTheta = Math.cos( theta );
  217.  
  218. // vertex
  219. vertex.x = radius * sinTheta;
  220. vertex.y = - v * height + halfHeight;
  221. vertex.z = radius * cosTheta;
  222. vertices.push( vertex.x, vertex.y, vertex.z );
  223.  
  224. // normal
  225. normal.set( sinTheta, slope, cosTheta ).normalize();
  226. normals.push( normal.x, normal.y, normal.z );
  227.  
  228. // uv
  229. uvs.push( 0.5, 1 - v );
  230.  
  231. // save index of vertex in respective row
  232. indexRow.push( index ++ );
  233. }
  234. indexRow.push(indexRow[0]);
  235.  
  236. // now save vertices of the row in our index array
  237. indexArray.push( indexRow );
  238. }
  239.  
  240. // generate indices
  241. for ( let x = 0; x < radialSegments; x ++ ) {
  242. for ( let y = 0; y < heightSegments; y ++ ) {
  243.  
  244. // we use the index array to access the correct indices
  245. const a = indexArray[ y ][ x ];
  246. const b = indexArray[ y + 1 ][ x ];
  247. const c = indexArray[ y + 1 ][ x + 1 ];
  248. const d = indexArray[ y ][ x + 1 ];
  249.  
  250. // faces
  251. if ( radiusTop > 0 || y !== 0 ) {
  252. indices.push( a, b, d );
  253. groupCount += 3;
  254. }
  255.  
  256. if ( radiusBottom > 0 || y !== heightSegments - 1 ) {
  257. indices.push( b, c, d );
  258. groupCount += 3;
  259. }
  260. }
  261. }
  262.  
  263. // add a group to the geometry. this will ensure multi material support
  264. scope.addGroup( groupStart, groupCount, 0 );
  265.  
  266. // calculate new start value for groups
  267. groupStart += groupCount;
  268. }
  269.  
  270. function generateCap( top ) {
  271. const normal = new Vector3();
  272.  
  273. // save the index of the first center vertex
  274. const centerIndexStart = index;
  275.  
  276. const uv = new Vector2();
  277. const vertex = new Vector3();
  278.  
  279. let groupCount = 0;
  280.  
  281. const radius = ( top === true ) ? radiusTop : radiusBottom;
  282. const sign = ( top === true ) ? 1 : - 1;
  283. // --- bullet-tip tuning ---
  284. const tipPower = 1.2; // >1 sharpens the tip; try 1.6–2.2
  285. const tipScaleY = 2.20; // >1 elongates the tip slightly along Y
  286.  
  287. const capIndex = [];
  288. capIndex.push( indexArray[ top ? 0 : heightSegments] )
  289. const segments = Math.floor(radialSegments / 4);
  290.  
  291. for (let y = 1; y < segments; y++) {
  292. const indexRow = [];
  293. const angle = y * Math.PI / 2 / segments;
  294. const sinAngle = Math.sin( angle );
  295. const cosAngle = Math.cos( angle );
  296.  
  297. for ( let x = 0; x < radialSegments; x ++ ) {
  298. const u = x / radialSegments;
  299. const theta = u * thetaLength + thetaStart;
  300. const sinTheta = Math.sin( theta );
  301. const cosTheta = Math.cos( theta );
  302.  
  303. // vertex.x = radius * sinTheta * cosAngle;
  304. // vertex.y = (halfHeight + sinAngle * radius) * sign;
  305. // vertex.z = radius * cosTheta * cosAngle;
  306. // remap hemisphere (sin/cos) → bullet-ish profile
  307. const sinP = Math.pow(sinAngle, tipPower);
  308. const cosP = Math.pow(cosAngle, tipPower);
  309. vertex.x = radius * sinTheta * cosP;
  310. vertex.y = (halfHeight + sinP * radius * tipScaleY) * sign;
  311. vertex.z = radius * cosTheta * cosP;
  312. vertices.push( vertex.x, vertex.y, vertex.z );
  313.  
  314. // normal.set(sinTheta * cosAngle, sinAngle, cosTheta * cosAngle).normalize();
  315. // approximate normal from the same remap (good enough for smooth shading?)
  316. normal.set(sinTheta * cosP, sinP, cosTheta * cosP).normalize();
  317. normals.push(normal.x, normal.y, normal.z);
  318.  
  319. uvs.push(0.5, top ? 1.0 : 0.0);
  320.  
  321. indexRow.push(index++);
  322. }
  323.  
  324. indexRow.push(indexRow[0]);
  325. capIndex.push(indexRow);
  326. }
  327.  
  328. vertices.push(0, (halfHeight + radius) * sign, 0);
  329. normals.push(0, sign, 0);
  330. uvs.push(0.5, top ? 1.0 : 0.0);
  331. const indexRow = [];
  332. for ( let x = 0; x <= radialSegments; x ++ ) indexRow.push(index);
  333. index++;
  334. capIndex.push(indexRow);
  335.  
  336. for (let y = 0; y < segments; y++) {
  337. for (let x = 0; x < radialSegments; x++) {
  338. const a = capIndex[y][x];
  339. const b = capIndex[y + 1][x];
  340. const c = capIndex[y + 1][x + 1];
  341. const d = capIndex[y][x + 1];
  342.  
  343. if (top) {
  344. indices.push(d, b, a);
  345. } else {
  346. indices.push(a, b, d);
  347. }
  348. groupCount += 3;
  349.  
  350. if (y !== segments - 1) {
  351. if (top) {
  352. indices.push(d, c, b);
  353. } else {
  354. indices.push(b, c, d);
  355. }
  356. groupCount += 3;
  357. }
  358. }
  359. }
  360.  
  361. // add a group to the geometry. this will ensure multi material support
  362. scope.addGroup( groupStart, groupCount, top === true ? 1 : 2 );
  363.  
  364. // calculate new start value for groups
  365. groupStart += groupCount;
  366. }
  367. }
  368.  
  369. copy( source ) {
  370. super.copy( source );
  371. this.parameters = Object.assign( {}, source.parameters );
  372. return this;
  373. }
  374.  
  375. static fromJSON( data ) {
  376. return new BladeGeometry( data.radiusTop, data.radiusBottom, data.height, data.radialSegments, data.heightSegments, data.openEnded, data.thetaStart, data.thetaLength );
  377. }
  378. };
  379.  
  380. //
  381. // Halo
  382. //
  383. const bladeHaloVertexShader = `
  384. varying vec3 vA;
  385. varying vec3 vB;
  386. varying vec3 vTarget;
  387.  
  388. vec3 PD(vec4 p) { return p.xyz; }
  389.  
  390. void main() {
  391. vA = PD(viewMatrix * modelMatrix * vec4( 0.0, -75.0, 0.0, 1.0));
  392. vB = PD(viewMatrix * modelMatrix * vec4( 0.0, 35.0, 0.0, 1.0));
  393. vTarget = PD(viewMatrix * modelMatrix * vec4(position, 1.0));
  394. gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
  395. }
  396. `;
  397.  
  398. const bladeHaloFragmentShader = `
  399. varying vec3 vA;
  400. varying vec3 vB;
  401. varying vec3 vTarget;
  402. uniform sampler2D iChannel0;
  403.  
  404. float line_dist(vec3 pt1, vec3 dir1, vec3 pt2, vec3 dir2) {
  405. vec3 n = normalize(cross(dir1, dir2));
  406. return abs(dot(n, pt1 - pt2));
  407. }
  408.  
  409. struct Ray { vec3 start, dir; };
  410. struct Plane { vec3 pos, normal; };
  411. struct Range { float begin, end; };
  412.  
  413. #define NOHIT 1000.0
  414. float FAR() { return NOHIT; }
  415. float NEAR() { return 0.0; }
  416. Range Everything() { return Range(NEAR(), FAR()); }
  417. Range Nothing() { return Range(FAR(), NEAR()); }
  418. float Max(float a, float b) { return max(a, b); }
  419. float Min(float a, float b) { return min(a, b); }
  420. bool Empty(Range a) { return a.begin >= a.end; }
  421.  
  422. Range Intersect(Range a, Range b) {
  423. if (Empty(a)) return Nothing();
  424. if (Empty(b)) return Nothing();
  425. Range ret = Range(Max(a.begin, b.begin), Min(a.end, b.end));
  426. if (Empty(ret)) return Nothing();
  427. return ret;
  428. }
  429.  
  430. Range Trace(Ray ray, Plane o) {
  431. vec3 pos = o.pos;
  432. vec3 normal = o.normal;
  433. float tmp = dot(pos - ray.start, normal);
  434. float div = dot(ray.dir, normal);
  435. if (div == 0.0) {
  436. if (tmp > 0.0) return Everything();
  437. else return Nothing();
  438. }
  439. float dist = tmp / div;
  440. if (div > 0.0) return Range(NEAR(), dist);
  441. else return Range(dist, FAR());
  442. }
  443.  
  444. float blade_dist(vec3 pt1, vec3 dir1_un) {
  445. vec3 dir1 = normalize(dir1_un);
  446. vec3 pt2 = vA;
  447. vec3 dir2 = vB - vA;
  448. vec3 n = normalize(cross(dir1, dir2));
  449. vec3 Q = normalize(cross(n, dir1));
  450. float p = clamp(Trace(Ray(vA, dir2), Plane(pt1, Q)).end, 0.0, 1.0);
  451. vec3 bp = pt2 + dir2 * p;
  452. float q = dot(bp - pt1, dir1);
  453. vec3 rp = pt1 + dir1 * q;
  454. return length(bp - rp);
  455. }
  456.  
  457. float get_point(vec3 pt1, vec3 dir1_un) {
  458. vec3 dir1 = normalize(dir1_un);
  459. vec3 pt2 = vA;
  460. vec3 dir2 = vB - vA;
  461. vec3 n = normalize(cross(dir1, dir2));
  462. vec3 Q = normalize(cross(n, dir1));
  463. float p = clamp(Trace(Ray(vA, dir2), Plane(pt1, Q)).end, 0.0, 1.0);
  464. return p;
  465. }
  466.  
  467. void main() {
  468. vec3 eye = vec3(0.0, 0.0, 0.0);
  469. vec3 dir = vTarget - eye;
  470. float dist = blade_dist(eye, dir);
  471. float flyby_pt = get_point(eye, dir);
  472. float cosA = length(cross(normalize(vB - vA), normalize(dir)));
  473. dist /= 30.0;
  474. vec3 haze_color = texture2D(
  475. iChannel0,
  476. vec2(1.0 - flyby_pt, sqrt(dist) / 2.0 / cosA)
  477. ).rgb;
  478. // vec3 haze_color = texture2D(iChannel0, vec2(flyby_pt, 1.0)).rgb;
  479. // haze_color /= (dist * dist * dist * dist * 500.0 + 1.0); // FH
  480. haze_color /= (dist * dist * 30.0 + 1.0); // BC likey
  481. // haze_color *= 1.0 - sqrt(dist);
  482. gl_FragColor = vec4(haze_color, 1.0);
  483. }
  484. `;
  485.  
  486. // good point
  487.  
  488. const max_haze_depth = 8;
  489. const blade_data = new Uint8Array(4 * 144);
  490. const haze_data = new Uint8Array(4 * 144 * max_haze_depth);
  491. var blade_texture;
  492. var haze_texture;
  493. const TRAIL_LENGTH = 100; // ~100
  494. let bladeTrailMeshes = [];
  495. window.bladeTrailTransforms = [];
  496. let bladeTrailMeshesReady = false;
  497. let trailCaptureInterval = 2;
  498. const LERP_STEPS = 50; //40-50 else it's distinct repeats
  499. let frameCounter = 0;
  500. const trailSpeedThreshold = 120; // 120
  501. let wasOverTrailThreshold = false;
  502. // let framesBelow = 0; // how many consecutive frames we’ve been under the speed threshold
  503.  
  504. function lerpMatrix4(m1, m2, alpha) {
  505. // Decompose matrices into position, quaternion, scale
  506. const pos1 = new THREE.Vector3(), quat1 = new THREE.Quaternion(), scale1 = new THREE.Vector3();
  507. m1.decompose(pos1, quat1, scale1);
  508. const pos2 = new THREE.Vector3(), quat2 = new THREE.Quaternion(), scale2 = new THREE.Vector3();
  509. m2.decompose(pos2, quat2, scale2);
  510. const mLerp = new THREE.Matrix4();
  511. mLerp.compose(
  512. pos1.clone().lerp(pos2, alpha),
  513. quat1.clone().slerp(quat2, alpha),
  514. scale1.clone().lerp(scale2, alpha)
  515. );
  516. return mLerp;
  517. }
  518.  
  519. var loader = new RGBELoader().setPath('./');
  520. // loader.load('ostrich_road_2k.hdr', function(texture) {
  521. // loader.load('hallway_sky2.hdr', function(texture) {
  522. loader.load('1965hallway_sky.hdr', function(texture) {
  523.  
  524. texture.mapping = THREE.EquirectangularReflectionMapping;
  525. // scene.background = texture;
  526. var envMap = texture;
  527.  
  528. // Background “painting” behind the saber
  529. bgUniforms = {
  530. envMap: { value: envMap },
  531. zoom: { value: 2.1 }, // >1 zooms in <1 zooms out
  532. brightness: { value: 3.0 }
  533. };
  534.  
  535. bgMaterial = new THREE.ShaderMaterial({
  536. uniforms: bgUniforms,
  537. vertexShader: `
  538. varying vec2 vUv;
  539. void main() {
  540. vUv = uv;
  541. gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  542. }
  543. `,
  544. fragmentShader: `
  545. uniform sampler2D envMap;
  546. uniform float zoom;
  547. uniform float brightness;
  548. varying vec2 vUv;
  549. void main() {
  550. vec2 uv = (vUv - 0.5) / zoom + 0.5;
  551. vec3 color = texture(envMap, uv).rgb;
  552. color *= brightness; // brighten
  553. gl_FragColor = vec4(color, 1.0);
  554. }
  555. `,
  556. depthWrite: false
  557. });
  558. createBgPlane();
  559.  
  560. const gltf_loader = new GLTFLoader();
  561. gltf_loader.load('obi/scene.gltf', function(gltf) {
  562. gltf.scene.traverse(function(child) {
  563. if (child.isMesh) {
  564. // console.log("MESH");
  565. child.material.envMap = envMap;
  566. }
  567. });
  568.  
  569. scene.add(gltf.scene);
  570. hilt = gltf.scene;
  571. window.hilt = hilt;
  572. hilt.position.set(0, 1.6, -200); // turns out this isn't used?
  573.  
  574. if (true) {
  575. // create a buffer with color data
  576.  
  577. const width = 1;
  578. const height = 144;
  579. const size = width * height;
  580.  
  581. blade_texture = new THREE.DataTexture(blade_data, width, height);
  582. blade_texture.colorSpace = THREE.LinearSRGBColorSpace;
  583. blade_texture.generateMipmaps = false;
  584. blade_texture.magFilter = THREE.LinearFilter;
  585. blade_texture.minFilter = THREE.LinearFilter;
  586. blade_texture.needsUpdate = true;
  587.  
  588. haze_texture = new THREE.DataTexture(haze_data, height, max_haze_depth);
  589. haze_texture.colorSpace = THREE.LinearSRGBColorSpace;
  590. haze_texture.generateMipmaps = false;
  591. haze_texture.magFilter = THREE.LinearFilter;
  592. haze_texture.minFilter = THREE.LinearFilter;
  593. haze_texture.needsUpdate = true;
  594.  
  595. const blade_translation = new THREE.Matrix4()
  596. .makeTranslation(0.0, -20, 0.0)
  597. .multiply(new THREE.Matrix4().makeRotationX(Math.PI));
  598.  
  599. const blade_geometry = new BladeGeometry(1.3, 1.3, 110, 64, 1);
  600. blade_geometry.applyMatrix4(blade_translation);
  601.  
  602. const blade_material = new THREE.MeshStandardMaterial({
  603. color: 0xCCCCCC,
  604. emissiveMap: blade_texture,
  605. emissiveIntensity: 1.7,
  606. emissive: 0xffffffff,
  607. envMap: envMap
  608. });
  609.  
  610. blade = new THREE.Mesh(blade_geometry, blade_material);
  611. hilt.add(blade);
  612.  
  613. // Trail
  614. for (let i = 0; i < TRAIL_LENGTH; ++i) {
  615. const trailMat = new THREE.MeshBasicMaterial({
  616. map: blade_texture, // use the same live texture as the blade
  617. transparent: true,
  618. opacity: 0.6, // base; we’ll fade per-ghost below
  619. blending: THREE.AdditiveBlending,
  620. depthWrite: false, // avoid z-fighting between ghosts
  621. side: THREE.DoubleSide // optional, in case geom is single-sided
  622. });
  623.  
  624. const trailBlade = new THREE.Mesh(blade_geometry, trailMat);
  625. scene.add(trailBlade);
  626. bladeTrailMeshes.push(trailBlade);
  627. }
  628. bladeTrailMeshesReady = true;
  629.  
  630. const blade_aura_geometry = new BladeGeometry(50, 50, 110, 16, 1);
  631. blade_aura_geometry.applyMatrix4(blade_translation);
  632.  
  633. const blade_aura_material = new THREE.MeshStandardMaterial({
  634. color: 0x11111111,
  635. opacity: 0.1,
  636. transparent: true,
  637. emissiveMap: blade_texture,
  638. emissiveIntensity: 0.1,
  639. emissive: 0xffffffff,
  640. envMap: envMap
  641. });
  642.  
  643. const bladeHaloUniforms = {
  644. // iTime: { value: 0 },
  645. // iResolution: { value: new THREE.Vector3(1, 1, 1) },
  646. iChannel0: { value: haze_texture }
  647. };
  648.  
  649. const blade_halo_material = new THREE.ShaderMaterial({
  650. vertexShader: bladeHaloVertexShader,
  651. fragmentShader: bladeHaloFragmentShader,
  652. uniforms: bladeHaloUniforms,
  653. transparent: true,
  654. blending: THREE.AdditiveBlending
  655. });
  656.  
  657. var blade_aura = new THREE.Mesh(blade_aura_geometry, blade_halo_material);
  658. hilt.add(blade_aura);
  659. }
  660. }, undefined, function(error) {
  661. console.error(error);
  662. });
  663. });
  664.  
  665.  
  666. if (false) {
  667. const geometry = new THREE.BoxGeometry( 1, 1, 1 );
  668. const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
  669. const cube = new THREE.Mesh( geometry, material );
  670. scene.add( cube );
  671. }
  672.  
  673. if (false) {
  674. const gltf_loader = new GLTFLoader();
  675. gltf_loader.load( 'public/obi/scene.gltf', function ( gltf ) {
  676. scene.add( gltf.scene );
  677. }, undefined, function ( error ) {
  678. console.error( error );
  679. } );
  680. }
  681. // camera.position.z = 200;
  682.  
  683. var Q = 0;
  684.  
  685. const start_millis = new Date().getTime();
  686. function actual_millis() {
  687. return new Date().getTime() - start_millis;
  688. }
  689.  
  690. function animate() {
  691. if (!bladeTrailMeshesReady) return;
  692.  
  693. // Ensure arrays exist
  694. window.bladeTrailTransforms = window.bladeTrailTransforms || [];
  695. bladeTrailMeshes = bladeTrailMeshes || [];
  696.  
  697. resizeCanvasAndCamera();
  698. Q++;
  699.  
  700. // --- LED + haze update (unchanged) ---
  701. if (blade_texture) {
  702. const pixels = window.getSaberColors();
  703. for (let i = 0; i < 144; i++) {
  704. const stride = i * 4;
  705. blade_data[stride ] = Math.round(255 * pixels[i*3 ]);
  706. blade_data[stride + 1] = Math.round(255 * pixels[i*3 + 1]);
  707. blade_data[stride + 2] = Math.round(255 * pixels[i*3 + 2]);
  708. blade_data[stride + 3] = 255;
  709. }
  710. blade_texture.needsUpdate = true;
  711.  
  712. const num_leds = 144;
  713. for (let depth = 0; depth < max_haze_depth; depth++) {
  714. for (let i = 0; i < num_leds; i++) {
  715. let R = 0, G = 0, B = 0, W = 0;
  716. const haze_dist = 1 + 4 * depth;
  717. for (let D = -64; D <= 64; D++) {
  718. let p = i + D;
  719. if (p < 0 || p >= num_leds) continue;
  720. const dist = (Math.abs(D) + 1) / haze_dist + 1;
  721. const wgt = 1 / (dist * dist);
  722. R += pixels[p*3 ] * wgt;
  723. G += pixels[p*3 + 1] * wgt;
  724. B += pixels[p*3 + 2] * wgt;
  725. W += wgt;
  726. }
  727. R /= W; G /= W; B /= W;
  728. const off = (i + depth * num_leds) * 4;
  729. haze_data[off ] = Math.round(R * 255);
  730. haze_data[off + 1] = Math.round(G * 255);
  731. haze_data[off + 2] = Math.round(B * 255);
  732. haze_data[off + 3] = 255;
  733. }
  734. }
  735. haze_texture.needsUpdate = true;
  736. }
  737.  
  738. // Smooth home‐reset
  739. if (HOME_POS) {
  740. let done = true;
  741. for (let i = 0; i < MOVE_MATRIX.values.length; i++) {
  742. const diff = default_move_matrix().values[i] - MOVE_MATRIX.values[i];
  743. MOVE_MATRIX.values[i] += diff * 0.15;
  744. if (Math.abs(diff) > 1) done = false;
  745. }
  746. if (done) {
  747. HOME_POS = false;
  748. MOVE_MATRIX = default_move_matrix();
  749. }
  750. }
  751.  
  752. const trailsEnabled = !!window.showBladeTrails && IN_FRAME;
  753. if (!trailsEnabled) {
  754. bladeTrailMeshes.forEach(m => { m.visible = false; });
  755. window.bladeTrailTransforms.length = 0;
  756. // framesBelow = 0;
  757. wasOverTrailThreshold = false;
  758. }
  759.  
  760. // Trail thresholds
  761. const START_TH = trailSpeedThreshold;
  762. const STOP_TH = (window.trailStopThreshold || START_TH * 0.6);
  763.  
  764. // Update hilt (always), capture/decay trails only when enabled
  765. if (hilt && blade) {
  766. const mat = window.getSaberMove();
  767. hilt.matrixAutoUpdate = false;
  768. hilt.matrix.fromArray(mat.values);
  769. hilt.updateMatrixWorld(true);
  770.  
  771. frameCounter++;
  772.  
  773. const spd = Math.max(0, lastSwingSpeed);
  774. const overThreshold = spd > START_TH;
  775. const coastCaptureInterval = Math.max(2, trailCaptureInterval * 2);
  776.  
  777. if (trailsEnabled) {
  778. if (overThreshold) {
  779. // // Prevent “retro” trails on rising edge
  780. // if (!wasOverTrailThreshold && framesBelow >= 4) {
  781. // window.bladeTrailTransforms.length = 0;
  782. // console.log("********************* HERE **********");
  783. // }
  784. // framesBelow = 0;
  785.  
  786. // Record only while fast
  787. if (frameCounter % trailCaptureInterval === 0) {
  788. const s = Math.max(0, Math.min(1, (spd - START_TH) / START_TH));
  789. blade.updateMatrixWorld(true);
  790. window.bladeTrailTransforms.push({ m: blade.matrixWorld.clone(), t: performance.now(), s });
  791. if (window.bladeTrailTransforms.length > TRAIL_LENGTH) {
  792. window.bladeTrailTransforms.shift();
  793. }
  794. }
  795. } else {
  796. // Under start threshold: do not hard‑clear; coast until truly slow
  797. // framesBelow++;
  798.  
  799. // While above STOP_TH, keep capturing but at a lower cadence
  800. if (spd > STOP_TH && (frameCounter % coastCaptureInterval === 0)) {
  801. const s = Math.max(0, Math.min(1, (spd - START_TH) / START_TH));
  802. blade.updateMatrixWorld(true);
  803. window.bladeTrailTransforms.push({ m: blade.matrixWorld.clone(), t: performance.now(), s });
  804. if (window.bladeTrailTransforms.length > TRAIL_LENGTH) {
  805. window.bladeTrailTransforms.shift();
  806. }
  807. }
  808.  
  809. // Gentle decay of very old frames while slow
  810. if (window.bladeTrailTransforms.length && frameCounter % 12 === 0) {
  811. window.bladeTrailTransforms.shift();
  812. }
  813. }
  814.  
  815. wasOverTrailThreshold = overThreshold;
  816. }
  817. }
  818.  
  819. // Reset blade trail visibility every frame before rendering !important!
  820. bladeTrailMeshes.forEach(m => m.visible = false);
  821.  
  822. // Hysteresis for showing trails (start vs stop)
  823. window.renderTrailsActive = window.renderTrailsActive || false;
  824. window.renderTrailsActive = lastSwingSpeed > (window.renderTrailsActive ? STOP_TH : START_TH);
  825.  
  826.  
  827. // Only render trails if toggle is ON, we’re “active”, and we have data
  828. if (trailsEnabled &&
  829. window.renderTrailsActive &&
  830. window.bladeTrailTransforms.length > 1 &&
  831. bladeTrailMeshes.length > 0) {
  832.  
  833. const baseLocal = new THREE.Vector3(0, 35, 0);
  834. const tipLocal = new THREE.Vector3(0, -75, 0);
  835.  
  836. // Track current emitter position for anchor
  837. const currentBaseWorld = baseLocal.clone().applyMatrix4(blade.matrixWorld);
  838.  
  839. // Interpolate trail matrices; also carry timestamps and strength
  840. const VM = [];
  841. const VMt = [];
  842. const VMs = [];
  843. const T = window.bladeTrailTransforms; // [{ m, t, s }, ...]
  844.  
  845. if (T.length === 1) {
  846. VM.push(T[0].m); VMt.push(T[0].t); VMs.push(T[0].s ?? 1.0);
  847. } else {
  848. for (let i = 0; i < T.length - 1; i++) {
  849. VM.push(T[i].m); VMt.push(T[i].t); VMs.push(T[i].s ?? 1.0);
  850. for (let s = 1; s <= LERP_STEPS; s++) {
  851. const u = s / (LERP_STEPS + 1);
  852. VM.push(lerpMatrix4(T[i].m, T[i+1].m, u));
  853. VMt.push(T[i].t + u * (T[i+1].t - T[i].t)); // interpolated time
  854. VMs.push((T[i].s ?? 1.0) + u * ((T[i+1].s ?? 1.0) - (T[i].s ?? 1.0))); // interpolated strength
  855. }
  856. }
  857. VM.push(T[T.length - 1].m);
  858. VMt.push(T[T.length - 1].t);
  859. VMs.push(T[T.length - 1].s ?? 1.0);
  860. }
  861.  
  862. // Constants for fading & pruning (hard-coded)
  863. const now = performance.now();
  864. const FADE_MS = 1000; // time from full → 0 opacity per ghost
  865. const BASE_OPA = 0.90; // starting opacity
  866. const PRUNE_MS = FADE_MS * 2;
  867.  
  868. // Prune totally invisible source frames
  869. const cutoff = now - PRUNE_MS;
  870. while (window.bladeTrailTransforms.length &&
  871. window.bladeTrailTransforms[0].t < cutoff) {
  872. window.bladeTrailTransforms.shift();
  873. }
  874.  
  875. // Render most-recent ghosts first; fade by age and scale by speed strength
  876. const meshes = bladeTrailMeshes;
  877. const count = Math.min(meshes.length, VM.length);
  878.  
  879. for (let k = 0; k < count; k++) {
  880. const idx = VM.length - 1 - k; // newest first
  881. const ghostMat = VM[idx];
  882.  
  883. // Pose from recorded frame (anchor locked to recorded base for stability)
  884. const oldBaseWorld = baseLocal.clone().applyMatrix4(ghostMat);
  885. const oldTipWorld = tipLocal.clone().applyMatrix4(ghostMat);
  886. // const anchor = oldBaseWorld;
  887. // Keep anchored at emitter end, but slightly loose to maintain taper
  888. const anchor = oldBaseWorld.clone().lerp(currentBaseWorld, 0.5);
  889.  
  890. const dir = oldTipWorld.clone().sub(anchor).normalize();
  891. const quat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, -1, 0), dir);
  892. const offset = baseLocal.clone().applyQuaternion(quat);
  893. const m4 = new THREE.Matrix4().compose(anchor.clone().sub(offset), quat, new THREE.Vector3(1, 1, 1));
  894.  
  895. const mesh = meshes[k];
  896. mesh.matrixAutoUpdate = false;
  897. mesh.visible = true;
  898. mesh.matrix.copy(m4);
  899.  
  900. // Age-based fade: ski-jump curve (fast initial drop, long tail)
  901. const ageMs = Math.max(0, now - VMt[idx]);
  902. const lifeT = Math.min(1, ageMs / FADE_MS); // 0 → 1 over FADE_MS
  903. const fade = Math.pow(1.0 - lifeT, 35); // steep early, smooth tail
  904.  
  905. // Speed-based strength (from recording): 0..1
  906. const strength = Math.max(0, Math.min(1, VMs[idx]));
  907.  
  908. mesh.material.opacity = BASE_OPA * strength * fade;
  909. mesh.material.needsUpdate = true;
  910.  
  911. if (mesh.material.opacity <= 0.01) mesh.visible = false;
  912. }
  913.  
  914. // Hide extra meshes not used this frame
  915. for (let k = count; k < meshes.length; k++) {
  916. meshes[k].visible = false;
  917. }
  918. }
  919.  
  920. // Final render - real blade always there
  921. renderer.render(scene, camera);
  922. }
  923. renderer.setAnimationLoop(animate);
  924.  
Advertisement
Add Comment
Please, Sign In to add comment