import * as THREE from 'three'; import {ENDLESS_BODY,type Solid} from './EndlessSolid'; import type {SoftPose} from './EndlessSoft'; import {ENDLESS_RAINBOW_GLSL,ENDLESS_CUT_GLSL} from '../../jelly/shaders/endless.glsl'; export interface BatchPiece {id:number;geometry:THREE.BufferGeometry;matrix:THREE.Matrix4;visualMatrix?:THREE.Matrix4;visible:boolean} /** 바닥 높이를 고정하고 부피를 보존하는 눌림·전단을 논리 변환 위에 얹는다. */ export function softenMatrix(matrix:THREE.Matrix4,bounds:THREE.Box3,pose:SoftPose,dirX:number,dirZ:number,out:THREE.Matrix4):THREE.Matrix4 { const sy=1+pose.squash,sx=1/Math.sqrt(sy),hx=pose.shear*dirX,hz=pose.shear*dirZ; const cx=(bounds.min.x+bounds.max.x)/2,cz=(bounds.min.z+bounds.max.z)/2,floor=bounds.min.y; return out.set(sx,hx,0,(1-sx)*cx-hx*floor,0,sy,0,(1-sy)*floor,0,hz,sx,(1-sx)*cz-hz*floor,0,0,0,1).multiply(matrix); } export function endlessGeometry(solid:Solid):THREE.BufferGeometry { const p:number[]=[],pattern:number[]=[],cut:number[]=[]; for(const face of solid.faces){ // 0=외피, 1/2/3=원본 X/Y/Z축 투영. GPU 미분으로 축을 다시 고르지 않는다. let projection=0; if(face.cut){ const normal=new THREE.Vector3(),a=new THREE.Vector3(),b=new THREE.Vector3(),origin=face.vertices[0].p; for(let i=1;i=y-epsilon&&x>=z-epsilon?1:y>=z-epsilon?2:3; } for(let i=1;inew THREE.Vector3(v.p.x,v.p.y,v.p.z)); if(new THREE.Vector3().subVectors(b,a).cross(new THREE.Vector3().subVectors(c,a)).lengthSq()<1e-18)continue; for(const v of tri){p.push(v.p.x,v.p.y,v.p.z);pattern.push(v.pattern.x,v.pattern.y,v.pattern.z);cut.push(projection);} } } const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));g.setAttribute('endlessPattern',new THREE.Float32BufferAttribute(pattern,3));g.setAttribute('endlessCut',new THREE.Float32BufferAttribute(cut,1));g.computeVertexNormals(); const normals=g.attributes.normal as THREE.BufferAttribute,n=new THREE.Vector3(); // 원본의 둥근 상자 법선을 외피에만 쓰고 새 절단면의 평평한 법선은 보존한다. for(let i=0;i1e-12){n.normalize();normals.setXYZ(i,n.x,n.y,n.z);} } g.computeBoundingBox();g.computeBoundingSphere();return g; } export function endlessMaterial():THREE.MeshPhysicalMaterial { const m=new THREE.MeshPhysicalMaterial({color:0xffffff,roughness:.10,clearcoat:.7,clearcoatRoughness:.07,specularIntensity:.45,transmission:.88,thickness:.48,ior:1.35,attenuationColor:0xffffff,attenuationDistance:1.8,metalness:0,envMapIntensity:.6}); m.onBeforeCompile=s=>{ s.vertexShader='attribute vec3 endlessPattern; attribute float endlessCut; varying vec3 ePattern; varying float eCut;\n'+s.vertexShader.replace('#include ','#include \nePattern=endlessPattern; eCut=endlessCut;'); s.fragmentShader=ENDLESS_RAINBOW_GLSL+ENDLESS_CUT_GLSL+'\nvarying vec3 ePattern; varying float eCut;\n'+s.fragmentShader; s.fragmentShader=s.fragmentShader.replace('#include ',`#include vec3 rainbow=endlessRainbow(ePattern); float cutMask=step(.5,eCut); vec3 stars=vec3(0.0); if(cutMask>.5)stars=endlessHologram(ePattern,eCut); float flakes=stars.x; vec3 flakeTint=endlessFoil(stars.y*6.2831853); diffuseColor.rgb=mix(rainbow,endlessCutColor(rainbow),cutMask); diffuseColor.rgb=mix(diffuseColor.rgb,flakeTint,flakes*.72); `); s.fragmentShader=s.fragmentShader.replace('#include ',`#include roughnessFactor=mix(roughnessFactor,mix(.10,.065,flakes),cutMask); `); s.fragmentShader=s.fragmentShader.replace('#include ',`#include float facing=dot(normal,normalize(vViewPosition)); float foilPhase=facing*10.0+stars.y*6.2831853+dot(ePattern,vec3(5.0,9.0,4.0)); float glint=pow(.5+.5*sin(facing*35.0+stars.y*47.0),8.0); totalEmissiveRadiance+=endlessFoil(foilPhase)*flakes*(.28+glint*1.25); vec3 bodyTint=mix(rainbow,vec3(.38)+(vec3(1.0)-rainbow)*.38,cutMask); float softRim=pow(1.0-clamp(abs(facing),0.0,1.0),2.0); totalEmissiveRadiance+=bodyTint*(.035+.045*softRim)*(1.0-flakes); `); // 어두운 표면과 투과광 색을 분리해 속빛을 남기고 홀로그램 박은 빛을 가리게 한다. s.fragmentShader=s.fragmentShader.replace('#include ',THREE.ShaderChunk.transmission_fragment .replace('material.transmission = transmission;','material.transmission = transmission * mix(1.0,.68,cutMask) * mix(1.0,.10,flakes);') .replace('vec3 pos = vWorldPosition;',`material.diffuseContribution=bodyTint; material.attenuationColor=mix(vec3(.62)+rainbow*.38,vec3(.55)+(vec3(1.0)-rainbow)*.45,cutMask); vec3 pos = vWorldPosition;`)); }; m.customProgramCacheKey=()=> 'endless-rainbow-translucent-stars-v7';return m; } /** 개별 형상은 CPU에 보존하고 보이는 조각만 하나의 GPU 버퍼로 묶는다. */ export class EndlessBatch { readonly geometry=new THREE.BufferGeometry(); readonly material=endlessMaterial(); readonly mesh=new THREE.Mesh(this.geometry,this.material); private capacity=0; private ranges=new Map(); private vector=new THREE.Vector3(); private normal=new THREE.Matrix3(); constructor(){this.mesh.name='endless-jelly-batch';this.mesh.castShadow=true;this.mesh.receiveShadow=true;this.geometry.boundingSphere=new THREE.Sphere(new THREE.Vector3(),8);} rebuild(pieces:Iterable):void { const visible=[...pieces].filter(p=>p.visible),total=visible.reduce((n,p)=>n+p.geometry.attributes.position.count,0); if(total>this.capacity){ this.geometry.dispose();this.capacity=Math.max(512,2**Math.ceil(Math.log2(total))); for(const [key,size]of [['position',3],['normal',3],['endlessPattern',3],['endlessCut',1]] as const)this.geometry.setAttribute(key,new THREE.BufferAttribute(new Float32Array(this.capacity*size),size).setUsage(THREE.DynamicDrawUsage)); } this.ranges.clear();let start=0; for(const p of visible){const count=p.geometry.attributes.position.count;this.ranges.set(p.id,{start,count});this.write(p);start+=count;} this.geometry.setDrawRange(0,total);this.flush(); } write(piece:BatchPiece):void { const range=this.ranges.get(piece.id);if(!range)return; const src=piece.geometry.attributes,matrix=piece.visualMatrix??piece.matrix;this.normal.getNormalMatrix(matrix); for(let i=0;ir.start)),end=Math.max(...a.updateRanges.map(r=>r.start+r.count)); a.clearUpdateRanges();a.addUpdateRange(start,end-start);a.needsUpdate=true; } } dispose():void{this.geometry.dispose();this.material.dispose();} }