first commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Atlas dark-feature sampling baseline
|
||||
|
||||
This folder preserves the pre-change src/shaders/mosaic.frag as baseline-mosaic.frag so the Option2 atlas behavior remains reproducible.
|
||||
|
||||
The focused change probes four fixed cardinal offsets inside one 8x8 atlas texel footprint during REGION_ATLAS construction. Each probe uses the existing projected scene UV for both source RGB and front-depth evidence. A probe is accepted only when its .80-.95 front-depth and .10-.20 luminance evidence are stronger than the center sample; the representative blend is capped at .4. Center, far, invalid, and uniform cells retain the old result.
|
||||
|
||||
The added regression is a source/math-only check in scripts/test-region-atlas-dark-sampling.mjs; it does not claim browser visual success. The extra cost is four RGB probes per atlas texel, plus the existing validity/evidence lookups for those probe UVs. The final display pass, palette fitting, optical path, history, and camera gain are unchanged.
|
||||
@@ -0,0 +1,234 @@
|
||||
uniform sampler2D uSceneTexture,uSceneDepth;
|
||||
uniform sampler2D uProtectionDepthMap;
|
||||
uniform mat4 uContentViewProjection,uContentInverseViewProjection;
|
||||
uniform mat4 uPetWorldToLocal;
|
||||
uniform vec3 uContentCameraPosition,uLightPosition,uBackgroundColor;
|
||||
uniform float uGlassZ,uSourceZ,uGridSize,uStrength,uMosaicStyle,uCurvature,uGlossStrength;
|
||||
uniform vec2 uViewOffset,uPetSize,uPaneSize;
|
||||
uniform vec4 uPaneRect;
|
||||
uniform sampler2D uRegionPalette;
|
||||
uniform sampler2D uRegionAtlas,uRegionHistory;
|
||||
uniform vec2 uRegionAtlasSize,uRegionGridSize,uRegionPaletteSize;
|
||||
uniform float uRegionStrength,uRegionAvailable;
|
||||
varying vec2 vUv;
|
||||
vec2 projectUv(vec3 p){vec4 c=uContentViewProjection*vec4(p,1.);float w=abs(c.w)<.0001?(c.w<0.?-.0001:.0001):c.w;return c.xy/w*.5+.5;}
|
||||
vec4 sampleScene(vec2 uv){return texture2D(uSceneTexture,clamp(uv,0.,1.));}
|
||||
vec4 samplePane(vec2 pane){return sampleScene(projectUv(vec3((pane-.5)*uPaneSize,uGlassZ)));}
|
||||
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
|
||||
vec2 hash2(vec2 p){return vec2(hash(p),hash(p+vec2(19.19,73.73)));}
|
||||
vec2 cellBlurStep(vec2 cell,vec2 cellSize,float style){
|
||||
if(style<=.5)return cellSize*mix(.006,.018,hash(cell+vec2(5.)));
|
||||
return cellSize*mix(.006,.012,hash(cell+vec2(5.)));
|
||||
}
|
||||
vec4 sampleCell(vec2 pane,vec2 sourceUv,float strength,vec2 cell,vec2 cellSize,float style){
|
||||
vec2 center=mix(pane,sourceUv,strength);
|
||||
vec2 blurOffset=strength*cellBlurStep(cell,cellSize,style);
|
||||
vec4 color;
|
||||
if(style<=.5){
|
||||
color=samplePane(center)*.6;
|
||||
color+=samplePane(center+vec2(blurOffset.x,0.))*.1;
|
||||
color+=samplePane(center-vec2(blurOffset.x,0.))*.1;
|
||||
color+=samplePane(center+vec2(0.,blurOffset.y))*.1;
|
||||
color+=samplePane(center-vec2(0.,blurOffset.y))*.1;
|
||||
}else{
|
||||
color=samplePane(center)*.4;
|
||||
color+=samplePane(center+vec2(blurOffset.x,0.))*.15;
|
||||
color+=samplePane(center-vec2(blurOffset.x,0.))*.15;
|
||||
color+=samplePane(center+vec2(0.,blurOffset.y))*.15;
|
||||
color+=samplePane(center-vec2(0.,blurOffset.y))*.15;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
float safeZ(float z){return abs(z)<.0001?(z<0.?-.0001:.0001):z;}
|
||||
vec3 bend(vec3 i,vec3 n){vec3 r=refract(i,n,.75);return length(r)<.0001?i:r;}
|
||||
float softLimit(float value,float limit){return value*limit/(limit+abs(value));}
|
||||
float surfaceCurvature(){float curvature=clamp(uCurvature,-1.,1.),excess=max(curvature-.65,0.);return curvature-(.25/.35)*excess*excess;}
|
||||
float signedCurvatureScale(){return clamp(surfaceCurvature()/.65,-1.25,1.25);}
|
||||
float regionDistance(vec3 a,vec3 b){vec3 d=a-b;float y=dot(d,vec3(.299,.587,.114));return dot(d,d)+.35*y*y;}
|
||||
float regionLuminance(vec3 c){return dot(c,vec3(.299,.587,.114));}
|
||||
vec3 regionColor(vec2 cell,float index){vec2 uv=(vec2(cell.x*5.+index,cell.y)+.5)/uRegionPaletteSize;return texture2D(uRegionPalette,uv).rgb;}
|
||||
float nearestRegion(vec3 color,vec2 cell){float best=0.,distance=regionDistance(color,regionColor(cell,0.));for(int i=1;i<4;i++){float candidate=regionDistance(color,regionColor(cell,float(i)));if(candidate<distance){distance=candidate;best=float(i);}}return best;}
|
||||
float nearestRegion(vec3 color,vec3 p0,vec3 p1,vec3 p2,vec3 p3){float best=0.,distance=regionDistance(color,p0);float candidate=regionDistance(color,p1);if(candidate<distance){distance=candidate;best=1.;}candidate=regionDistance(color,p2);if(candidate<distance){distance=candidate;best=2.;}candidate=regionDistance(color,p3);if(candidate<distance)best=3.;return best;}
|
||||
vec3 selectRegion(vec3 p0,vec3 p1,vec3 p2,vec3 p3,float index){if(index<.5)return p0;if(index<1.5)return p1;if(index<2.5)return p2;return p3;}
|
||||
vec3 regionAtlasColor(vec2 cell,vec2 texel){vec2 uv=(cell*8.+clamp(texel,vec2(0.),vec2(7.))+.5)/uRegionAtlasSize;return texture2D(uRegionAtlas,uv).rgb;}
|
||||
bool reconstructScenePoint(vec2 sceneUv,out vec3 worldPoint){
|
||||
if(sceneUv.x<0.||sceneUv.x>1.||sceneUv.y<0.||sceneUv.y>1.)return false;
|
||||
float depth=texture2D(uSceneDepth,sceneUv).x;
|
||||
if(depth>=.999999)return false;
|
||||
vec4 clip=vec4(sceneUv*2.-1.,depth*2.-1.,1.);
|
||||
vec4 world=uContentInverseViewProjection*clip;
|
||||
if(abs(world.w)<.0001)return false;
|
||||
worldPoint=world.xyz/world.w;
|
||||
return true;
|
||||
}
|
||||
float regionSurfaceValid(vec2 sceneUv,vec3 glassHit){
|
||||
vec3 worldPoint;
|
||||
if(!reconstructScenePoint(sceneUv,worldPoint))return 0.;
|
||||
float axialGap=glassHit.z-worldPoint.z;
|
||||
if(axialGap<=.0001)return 0.;
|
||||
return 1.;
|
||||
}
|
||||
float regionProtectionWeight(vec2 sceneUv){
|
||||
vec3 worldPoint;
|
||||
if(!reconstructScenePoint(sceneUv,worldPoint))return 0.;
|
||||
vec3 localPoint=(uPetWorldToLocal*vec4(worldPoint,1.)).xyz;
|
||||
if(localPoint.x<-.5||localPoint.x>.5||localPoint.y<-.5||localPoint.y>.5)return 0.;
|
||||
vec2 sourceUv=localPoint.xy+.5;
|
||||
float sourceDepth=texture2D(uProtectionDepthMap,sourceUv).r;
|
||||
return smoothstep(.8,.95,sourceDepth);
|
||||
}
|
||||
void evaluateCell(vec2 cell,vec2 local,vec2 grid,vec2 cellSize,vec2 cellDimensions,float cellWorld,float opticalTravel,float style,out vec2 affineUv,out mat2 transform,out vec2 curvedSlope,out vec2 facetSlope){
|
||||
vec2 random=hash2(cell),widths;
|
||||
if(style>.5){
|
||||
widths=mix(vec2(.82),vec2(1.12),hash2(cell+vec2(11.4,9.8)));
|
||||
float angle=(random.x-.5)*.08,c=cos(angle),s=sin(angle);
|
||||
transform=mat2(widths.x*c,-widths.y*s,widths.x*s,widths.y*c);
|
||||
float centerJitter=.16;
|
||||
vec2 sourceCenter=(cell+.5)/grid+(random-.5)*centerJitter*cellSize;
|
||||
affineUv=sourceCenter+transform*(local*cellSize);
|
||||
}else{
|
||||
// Default style keeps a single continuous image footprint across cells.
|
||||
transform=mat2(1.);
|
||||
affineUv=(cell+.5)/grid+local*cellSize;
|
||||
}
|
||||
|
||||
// Positive height bulges toward the observer (+Z); negative height is concave.
|
||||
float curvature=clamp(uCurvature,-1.,1.),surfaceCurvatureValue=surfaceCurvature();
|
||||
vec2 q=clamp(2.*local,-1.,1.),q2=q*q,q3=q2*q,b=1.-q2*q2;
|
||||
if(style>.5){
|
||||
float amplitude=mix(.92,1.08,hash(cell+vec2(41.7,17.3))),height=.009*cellWorld*surfaceCurvatureValue*amplitude;
|
||||
vec2 skew=(hash2(cell+vec2(23.8,61.2))-.5)*.28;float asym=1.+dot(skew,q);
|
||||
vec2 dhdq=height*vec2(-4.*q3.x*b.y*asym+b.x*b.y*skew.x,-4.*q3.y*b.x*asym+b.x*b.y*skew.y);
|
||||
vec2 grad=-2.*dhdq/cellDimensions;
|
||||
curvedSlope=grad+(random-.5)*.012;facetSlope=(random-.5)*.012;
|
||||
}else{
|
||||
curvedSlope=.30*cellDimensions/max(abs(opticalTravel),.6)*signedCurvatureScale()*sin(6.2831853*local);
|
||||
facetSlope=vec2(0.);
|
||||
}
|
||||
}
|
||||
void main(){
|
||||
vec2 screenUv=vUv;
|
||||
#ifdef REGION_ATLAS
|
||||
vec2 atlasPixel=floor(vUv*uRegionAtlasSize),atlasCell=floor(atlasPixel/8.),atlasLocal=(mod(atlasPixel,8.)+.5)/8.;
|
||||
vec2 atlasPaneUv=(atlasCell+atlasLocal)/uRegionGridSize;
|
||||
screenUv=projectUv(vec3((atlasPaneUv-.5)*uPaneSize,uGlassZ));
|
||||
#endif
|
||||
if(uStrength<=.001){
|
||||
#ifdef REGION_ATLAS
|
||||
gl_FragColor=vec4(sampleScene(screenUv).rgb,0.);
|
||||
#else
|
||||
gl_FragColor=sampleScene(screenUv);
|
||||
#include <colorspace_fragment>
|
||||
#endif
|
||||
return;}
|
||||
vec4 f=uContentInverseViewProjection*vec4(screenUv*2.-1.,1.,1.);float fw=abs(f.w)<.0001?(f.w<0.?-.0001:.0001):f.w;vec3 farWorld=f.xyz/fw;vec3 i=normalize(farWorld-uContentCameraPosition);float gt=(uGlassZ-uContentCameraPosition.z)/safeZ(i.z);vec3 gh=uContentCameraPosition+i*gt;
|
||||
vec2 paneUv=gh.xy/uPaneSize+.5;vec2 grid=vec2(uGridSize,max(1.,uGridSize*uPaneSize.y/uPaneSize.x));vec2 gridUv=paneUv*grid,cell=floor(gridUv),local=fract(gridUv)-.5,cellSize=1./grid;float style=clamp(uMosaicStyle,0.,1.),cellWorld=min(uPaneSize.x/grid.x,uPaneSize.y/grid.y);
|
||||
#ifdef REGION_ATLAS
|
||||
paneUv=atlasPaneUv;grid= uRegionGridSize;gridUv=paneUv*grid;cell=floor(gridUv);local=fract(gridUv)-.5;cellSize=1./grid;cellWorld=min(uPaneSize.x/grid.x,uPaneSize.y/grid.y);
|
||||
#endif
|
||||
if(paneUv.x<0.||paneUv.x>1.||paneUv.y<0.||paneUv.y>1.){
|
||||
#ifdef REGION_ATLAS
|
||||
gl_FragColor=vec4(uBackgroundColor,0.);
|
||||
#else
|
||||
gl_FragColor=vec4(uBackgroundColor,1.);
|
||||
#include <colorspace_fragment>
|
||||
#endif
|
||||
return;}
|
||||
vec2 cellDimensions=uPaneSize/grid;
|
||||
float opticalTravel=-max(abs(uSourceZ-gh.z),.6);
|
||||
vec2 affineUv,curvedSlope,facetSlope;mat2 transform;
|
||||
evaluateCell(cell,local,grid,cellSize,cellDimensions,cellWorld,opticalTravel,style,affineUv,transform,curvedSlope,facetSlope);
|
||||
if(style<=.5){
|
||||
vec2 smoothFacet=.003*vec2(sin(6.2831853*paneUv.x),sin(6.2831853*paneUv.y));
|
||||
curvedSlope+=smoothFacet;facetSlope=smoothFacet;
|
||||
}else{
|
||||
vec2 edge=.5-abs(local),neighborSign=mix(vec2(-1.),vec2(1.),step(vec2(0.),local));
|
||||
float normalBlend=.16;
|
||||
vec2 weights=1.-smoothstep(vec2(-normalBlend),vec2(normalBlend),edge);
|
||||
float xSplitWeight=weights.x*(1.-weights.y),ySplitWeight=(1.-weights.x)*weights.y;
|
||||
float diagonalSplitWeight=weights.x*weights.y,centerSplitWeight=(1.-weights.x)*(1.-weights.y);
|
||||
if(weights.x>0.||weights.y>0.){
|
||||
vec2 neighborAffine,neighborCurved,neighborFacet;mat2 neighborTransform;
|
||||
vec2 xWeight=vec2(xSplitWeight,0.);
|
||||
vec2 yWeight=vec2(0.,ySplitWeight);
|
||||
affineUv*=centerSplitWeight;transform*=centerSplitWeight;curvedSlope*=centerSplitWeight;facetSlope*=centerSplitWeight;
|
||||
if(xWeight.x>0.){
|
||||
vec2 offset=vec2(neighborSign.x,0.);
|
||||
evaluateCell(cell+offset,local-offset,grid,cellSize,cellDimensions,cellWorld,opticalTravel,style,neighborAffine,neighborTransform,neighborCurved,neighborFacet);
|
||||
affineUv+=neighborAffine*xWeight.x;transform+=neighborTransform*xWeight.x;curvedSlope+=neighborCurved*xWeight.x;facetSlope+=neighborFacet*xWeight.x;
|
||||
}
|
||||
if(yWeight.y>0.){
|
||||
vec2 offset=vec2(0.,neighborSign.y);
|
||||
evaluateCell(cell+offset,local-offset,grid,cellSize,cellDimensions,cellWorld,opticalTravel,style,neighborAffine,neighborTransform,neighborCurved,neighborFacet);
|
||||
affineUv+=neighborAffine*yWeight.y;transform+=neighborTransform*yWeight.y;curvedSlope+=neighborCurved*yWeight.y;facetSlope+=neighborFacet*yWeight.y;
|
||||
}
|
||||
if(diagonalSplitWeight>0.){
|
||||
vec2 offset=neighborSign;
|
||||
evaluateCell(cell+offset,local-offset,grid,cellSize,cellDimensions,cellWorld,opticalTravel,style,neighborAffine,neighborTransform,neighborCurved,neighborFacet);
|
||||
affineUv+=neighborAffine*diagonalSplitWeight;transform+=neighborTransform*diagonalSplitWeight;curvedSlope+=neighborCurved*diagonalSplitWeight;facetSlope+=neighborFacet*diagonalSplitWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
vec3 nCurved=normalize(vec3(curvedSlope,1.)),nFacet=normalize(vec3(facetSlope,1.));
|
||||
vec3 straightHit=gh+i*((uSourceZ-gh.z)/safeZ(i.z));
|
||||
vec3 curvedRay=bend(i,nCurved),curvedHit=gh+curvedRay*((uSourceZ-gh.z)/safeZ(curvedRay.z));
|
||||
// Keep the artistic Mosaic optical path independent of physical glass-to-photo contact distance.
|
||||
vec2 rawDelta=(curvedRay.xy/safeZ(curvedRay.z)-i.xy/safeZ(i.z))*opticalTravel;
|
||||
vec2 worldDelta=vec2(softLimit(rawDelta.x,cellWorld*.55),softLimit(rawDelta.y,cellWorld*.55));
|
||||
if(style>.5){
|
||||
vec3 facetRay=bend(i,nFacet);vec3 facetHit=gh+facetRay*((uSourceZ-gh.z)/safeZ(facetRay.z));
|
||||
vec2 base=facetHit.xy-straightHit.xy,curve=curvedHit.xy-facetHit.xy;
|
||||
worldDelta=vec2(softLimit(base.x,cellWorld*.08),softLimit(base.y,cellWorld*.08));
|
||||
vec2 d=curve/cellWorld;
|
||||
worldDelta+=vec2(.3*d.x/(.3+abs(d.x)),.3*d.y/(.3+abs(d.y)))*cellWorld;
|
||||
}else{
|
||||
vec3 referenceI=normalize(gh-vec3(0.,0.,uContentCameraPosition.z)),referenceRay=bend(referenceI,nCurved);
|
||||
vec2 incidenceDelta=curvedRay.xy/max(-curvedRay.z,.0001)-referenceRay.xy/max(-referenceRay.z,.0001);
|
||||
vec2 boundedIncidence=incidenceDelta/sqrt(incidenceDelta*incidenceDelta+vec2(.025*.025));
|
||||
vec2 cellEnvelope=.5+.5*cos(6.2831853*local);
|
||||
worldDelta+=boundedIncidence*cellEnvelope*cellDimensions*signedCurvatureScale()*.18;
|
||||
}
|
||||
vec2 sampleUv=affineUv+transform*(worldDelta/uPaneSize);
|
||||
float edgeDistance=min(min(paneUv.x,paneUv.y),min(1.-paneUv.x,1.-paneUv.y));float edgeTaper=style<=.5?1.25:.75;float edgeWeight=smoothstep(0.,max(cellSize.x,cellSize.y)*edgeTaper,edgeDistance);float strength=clamp(uStrength,0.,1.)*edgeWeight;
|
||||
vec4 color;vec3 sceneHit=straightHit+vec3(worldDelta*strength,0.);vec2 finalSceneUv=projectUv(sceneHit);
|
||||
if(style<=.5){
|
||||
color=sampleScene(finalSceneUv);
|
||||
}else{
|
||||
color=sampleCell(paneUv,sampleUv,strength,cell,cellSize,style);
|
||||
}
|
||||
#ifndef REGION_ATLAS
|
||||
if(uRegionAvailable>.5&&uRegionStrength>.001&&uMosaicStyle<.5){
|
||||
vec3 p0=regionColor(cell,0.),p1=regionColor(cell,1.),p2=regionColor(cell,2.),p3=regionColor(cell,3.);
|
||||
float ownLabel=nearestRegion(color.rgb,p0,p1,p2,p3);vec4 votes=vec4(0.);float totalWeight=0.,ownWeight=0.;
|
||||
vec2 texelCoord=fract(gridUv)*8.-.5,anchor=floor(texelCoord+.5);
|
||||
for(int y=-1;y<=1;y++)for(int x=-1;x<=1;x++){
|
||||
vec2 offset=vec2(float(x),float(y)),delta=anchor+offset-texelCoord;
|
||||
float weight=pow(max(0.,1.-length(delta)/1.5),2.);
|
||||
if(weight>0.){
|
||||
vec3 atlasColor=regionAtlasColor(cell,anchor+offset);float label=nearestRegion(atlasColor,p0,p1,p2,p3);
|
||||
votes+=vec4(step(abs(label-vec4(0.,1.,2.,3.)),vec4(.1)))*weight;totalWeight+=weight;
|
||||
ownWeight+=step(abs(label-ownLabel),.1)*weight;
|
||||
}
|
||||
}
|
||||
float winningLabel=ownLabel,winningWeight=ownWeight;
|
||||
if(votes.x>winningWeight){winningLabel=0.;winningWeight=votes.x;}if(votes.y>winningWeight){winningLabel=1.;winningWeight=votes.y;}if(votes.z>winningWeight){winningLabel=2.;winningWeight=votes.z;}if(votes.w>winningWeight){winningLabel=3.;winningWeight=votes.w;}
|
||||
bool simplify=totalWeight>0.&&winningWeight/totalWeight>.55&&ownWeight/totalWeight<.40;
|
||||
float radialLabel=simplify?winningLabel:ownLabel;vec3 radialColor=selectRegion(p0,p1,p2,p3,radialLabel);
|
||||
vec2 historyTexel=clamp(floor(fract(gridUv)*8.),vec2(0.),vec2(7.));
|
||||
vec2 historyUv=(cell*8.+historyTexel+.5)/uRegionAtlasSize;
|
||||
vec4 splitHistory=texture2D(uRegionHistory,historyUv);
|
||||
vec3 splitColor=splitHistory.a>.00001?splitHistory.rgb/splitHistory.a:radialColor;
|
||||
vec3 candidateColor=mix(radialColor,splitColor,splitHistory.a);
|
||||
color.rgb=mix(color.rgb,candidateColor,clamp(uRegionStrength*clamp(uStrength,0.,1.)*regionSurfaceValid(finalSceneUv,gh),0.,1.));
|
||||
}
|
||||
#endif
|
||||
#ifndef REGION_ATLAS
|
||||
color.a=1.;
|
||||
vec3 v=normalize(uContentCameraPosition-gh),reflected=reflect(-v,nCurved); float lightT=(uLightPosition.z-gh.z)/safeZ(reflected.z); vec2 lightHit=gh.xy+reflected.xy*lightT; vec2 halfSize=vec2(.32,.42)*min(uPaneSize.x,uPaneSize.y); vec2 windowCoord=abs(lightHit-uLightPosition.xy)/halfSize; vec2 windowMask=1.-smoothstep(vec2(.45),vec2(1.15),windowCoord); float spec=windowMask.x*windowMask.y*step(.0001,reflected.z)*step(0.,lightT); float gloss=spec*clamp(uGlossStrength,0.,1.)*.12*edgeWeight*clamp(uStrength,0.,1.); color.rgb=mix(color.rgb,vec3(1.),gloss); gl_FragColor=color;
|
||||
#include <colorspace_fragment>
|
||||
#else
|
||||
color.a=regionProtectionWeight(finalSceneUv)*(1.-smoothstep(.10,.20,regionLuminance(color.rgb)));
|
||||
gl_FragColor=color;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 창문 뒤 반려동물 경험 개선 검증
|
||||
|
||||
2026-09-22. 비교 기준은 `docs/ref1-option-2.md`의 Ref1 옵션 2다.
|
||||
|
||||
## 적용 내용
|
||||
|
||||
- `src/objects/WindowReveal.ts`, `src/main.ts`: 앞쪽 창문 개구부와 사진의 기준 평면 사이를 연결하는 네 면의 안쪽 창틀을 추가했다. 뒤쪽 사각형은 수동 확대·팬과 독립적이며, 사진 구도나 카메라 이동 한계를 보정 수단으로 바꾸지 않는다. 형상은 구도·깊이·리사이즈 변경 시 갱신하며 기존 버퍼를 재사용한다.
|
||||
- `src/shaders/mosaic.frag`: 기본 Mosaic의 REGION_ATLAS에서 표본당 고정된 주변 네 지점을 추가로 조사한다. 같은 장면 UV에서 RGB와 앞쪽 깊이 증거를 읽으며, 더 강한 가까운 어두운 특징이 있을 때 대표색 혼합을 최대 0.4로 제한한다. 최종 화면의 원본 눈·코 픽셀 복원은 추가하지 않았다.
|
||||
- `index.html`, `src/main.ts`, `src/input/PointerController.ts`, `src/style.css`: 안내를 “화면을 드래그해서 시점을 움직여보세요”로 명시했다. 안내는 첫 유효 드래그까지 유지하고 새 미디어에서 다시 표시한다. 같은 미디어에서 이미 드래그했다면 AI 깊이 완료가 안내를 다시 띄우지 않는다. 드래그 중 커서도 구분한다.
|
||||
|
||||
기존 Grid 13, 굴곡 +1, 광택 0.30, 색면·굴절 강도 1, Camera Offset 기본 0.50/최대 0.90, 70ms history 및 DPR 상한 1.5를 유지한다. 사진은 기존의 단일 깊이 변위 표면이다.
|
||||
|
||||
## 실행 검증
|
||||
|
||||
`npm.cmd run build` 통과. TypeScript와 Vite 빌드가 완료됐으며 메인 청크 524.46 kB에 대한 기존 500 kB 초과 경고는 남아 있다.
|
||||
|
||||
다음 여섯 검사 각각 통과:
|
||||
|
||||
```text
|
||||
node --experimental-strip-types scripts/test-camera-history.mjs
|
||||
node --experimental-strip-types scripts/test-region-axis-split.mjs
|
||||
node --experimental-strip-types scripts/test-mosaic-depth-distance.mjs
|
||||
node --experimental-strip-types scripts/test-mosaic-continuity.mjs
|
||||
node --experimental-strip-types scripts/test-region-atlas-dark-sampling.mjs
|
||||
node --experimental-strip-types scripts/test-window-reveal-coverage.mjs
|
||||
```
|
||||
|
||||
창틀 검사는 실제 Three.js 메시와 Raycaster를 사용한다. 종횡비 0.7/1.6, 깊이 강도 0/0.25/0.4, 정면 및 ±0.5/±0.9의 가로·세로·대각선 조합 17개에서 개구부 광선 49개씩, 총 4,998개를 확인했다. 기존 네 개의 얕은 창틀 박스와 사진만으로는 146개가 빗나갔고 새 안쪽 면과 사진으로는 모두 교차했다. 형상·위치 버퍼 재사용과 형상별 한 번의 dispose 이벤트도 확인했다. 이는 정해진 형상 표본의 검사이며 모든 실제 사진·렌더링 결과를 보장하지 않는다.
|
||||
|
||||
어두운 특징 검사는 production GLSL의 연결·표본 수·문턱값을 확인하고, 복제한 JavaScript 계산으로 합성 좁은 어두운 특징과 먼 표면·무효 표면·균일 색의 결과를 검사한다. 합성 검사는 GLSL을 실행하는 GPU 검사가 아니며 사진별 시각 품질 개선의 증명으로 해석하지 않는다.
|
||||
|
||||
## 브라우저에서 확인한 범위
|
||||
|
||||
- dog1의 자동 AI 상대 깊이 생성 완료, 정면 Final에서 큰 사각 색면과 두 눈·코 표현을 확인했다.
|
||||
- AI 작업 완료 후에도 안내가 유지됐고, 실제 드래그 후 안내 opacity가 0으로 바뀌었다.
|
||||
- No Glass의 정면과 우측 이동 화면, 같은 이동 상태의 Final을 확인했다. Camera Offset 0.50 및 0.90 설정에서 안쪽 창틀 면과 사진이 접하는 결과를 확인했다. 이번 성공한 드래그의 정규화 이동은 약 +0.8이므로 ±1 끝점 전체의 브라우저 검증으로 기록하지 않는다.
|
||||
- 다른 기존 샘플로 변경한 뒤 안내가 다시 표시되고 “수작업 테스트 Depth · AI 추정 아님”이 유지됨을 확인했다.
|
||||
- 조회한 콘솔 로그에 실행 오류는 없었다. 기존 셰이더 X3595 경고(가변 반복문 안 gradient 연산)는 남아 있다.
|
||||
|
||||
일부 추가 브라우저 드래그는 자동 승인 검토에서 보안 상태를 결정하지 못해 차단됐다. 전체 좌우·상하·대각선 끝점은 위 형상 검사에서 검증했으며 전체 실화면 검증은 완료하지 못했다. 모바일 FPS, 연속 영상의 깜빡임 계측, 실제 변위 정점의 유리 간격 측정도 이번 검증에 포함하지 않는다.
|
||||
|
||||
## 비교 자료와 한계
|
||||
|
||||
수정 전 Mosaic 셰이더는 `baseline-mosaic.frag`에 보존되어 있다. UI·창틀의 수정 전 스냅샷은 없으며, 이번 검증에서 수정 전후의 동일 프레임 GPU 비교는 실행하지 않았다. 작은 눈·코가 모든 사진과 시점에서 보존된다고 보장하지 않는다. 숨겨진 옆면이나 배경을 복원하는 기능은 추가하지 않았다.
|
||||
Reference in New Issue
Block a user