first commit
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,274 @@
|
||||
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);
|
||||
}
|
||||
#ifdef REGION_ATLAS
|
||||
float regionAtlasEvidence(vec2 sceneUv,vec3 color,vec3 glassHit){
|
||||
if(regionSurfaceValid(sceneUv,glassHit)<=0.)return 0.;
|
||||
return regionProtectionWeight(sceneUv)*(1.-smoothstep(.10,.20,regionLuminance(color)));
|
||||
}
|
||||
vec4 regionAtlasProbe(vec3 sceneHit,vec2 worldOffset,vec3 glassHit){
|
||||
vec2 probeSceneUv=projectUv(sceneHit+vec3(worldOffset,0.));
|
||||
vec4 probe=sampleScene(probeSceneUv);
|
||||
return vec4(probe.rgb,regionAtlasEvidence(probeSceneUv,probe.rgb,glassHit));
|
||||
}
|
||||
vec4 regionAtlasScore(vec3 centerColor,float centerEvidence,vec3 probeColor,float probeEvidence){
|
||||
float evidenceGain=probeEvidence-centerEvidence,darkGain=regionLuminance(centerColor)-regionLuminance(probeColor),blend=0.;
|
||||
if(probeEvidence>.05&&evidenceGain>.02&&darkGain>.015)blend=min(.4,evidenceGain*2.)*min(1.,max(0.,darkGain*4.));
|
||||
return vec4(blend,probeEvidence,0.,0.);
|
||||
}
|
||||
vec3 regionAtlasRepresentative(vec3 centerColor,float centerEvidence,vec3 sceneHit,vec3 glassHit,out float evidence){
|
||||
vec3 bestColor=centerColor;float bestEvidence=centerEvidence,bestBlend=0.;
|
||||
vec2 probeStep=uPaneSize/(uRegionGridSize*8.)*.25;
|
||||
vec4 probe,score;
|
||||
probe=regionAtlasProbe(sceneHit,vec2(probeStep.x,0.),glassHit);score=regionAtlasScore(centerColor,centerEvidence,probe.rgb,probe.a);if(score.x>bestBlend){bestColor=mix(centerColor,probe.rgb,score.x);bestEvidence=max(centerEvidence,probe.a);bestBlend=score.x;}
|
||||
probe=regionAtlasProbe(sceneHit,vec2(-probeStep.x,0.),glassHit);score=regionAtlasScore(centerColor,centerEvidence,probe.rgb,probe.a);if(score.x>bestBlend){bestColor=mix(centerColor,probe.rgb,score.x);bestEvidence=max(centerEvidence,probe.a);bestBlend=score.x;}
|
||||
probe=regionAtlasProbe(sceneHit,vec2(0.,probeStep.y),glassHit);score=regionAtlasScore(centerColor,centerEvidence,probe.rgb,probe.a);if(score.x>bestBlend){bestColor=mix(centerColor,probe.rgb,score.x);bestEvidence=max(centerEvidence,probe.a);bestBlend=score.x;}
|
||||
probe=regionAtlasProbe(sceneHit,vec2(0.,-probeStep.y),glassHit);score=regionAtlasScore(centerColor,centerEvidence,probe.rgb,probe.a);if(score.x>bestBlend){bestColor=mix(centerColor,probe.rgb,score.x);bestEvidence=max(centerEvidence,probe.a);bestBlend=score.x;}
|
||||
evidence=bestEvidence;
|
||||
return bestColor;
|
||||
}
|
||||
#endif
|
||||
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);
|
||||
#ifdef REGION_ATLAS
|
||||
vec3 atlasCenterColor=sampleScene(finalSceneUv).rgb;
|
||||
float atlasEvidence=regionProtectionWeight(finalSceneUv)*(1.-smoothstep(.10,.20,regionLuminance(atlasCenterColor)));
|
||||
#endif
|
||||
#ifdef REGION_ATLAS
|
||||
if(style<=.5){
|
||||
float representativeEvidence=atlasEvidence;
|
||||
color=vec4(regionAtlasRepresentative(atlasCenterColor,atlasEvidence,sceneHit,gh,representativeEvidence),representativeEvidence);
|
||||
atlasEvidence=representativeEvidence;
|
||||
}else{
|
||||
color=sampleCell(paneUv,sampleUv,strength,cell,cellSize,style);
|
||||
}
|
||||
#else
|
||||
if(style<=.5){
|
||||
color=sampleScene(finalSceneUv);
|
||||
}else{
|
||||
color=sampleCell(paneUv,sampleUv,strength,cell,cellSize,style);
|
||||
}
|
||||
#endif
|
||||
#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=atlasEvidence;
|
||||
gl_FragColor=color;
|
||||
#endif
|
||||
}
|
||||
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,206 @@
|
||||
# Surface reflection implementation report
|
||||
|
||||
## Status and scope
|
||||
|
||||
Implemented Task 1 from `.superpowers/sdd/surface-reflection/task-1-brief.md`.
|
||||
The change is limited to the final non-`REGION_ATLAS` reflection composition in
|
||||
`src/shaders/mosaic.frag` plus one source/math regression script. Existing
|
||||
uniforms were sufficient, so `src/main.ts` was not changed.
|
||||
|
||||
The exact pre-edit shader is preserved at
|
||||
`docs/qa-surface-reflection/baseline-mosaic.frag`.
|
||||
|
||||
Baseline SHA-256:
|
||||
|
||||
```text
|
||||
CEE5F75C7A62A9146CBE451C661C2E0066EE57EB8963A7B0F56C281873F37E35
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
Changed files:
|
||||
|
||||
- `src/shaders/mosaic.frag`
|
||||
- `scripts/test-surface-reflection.mjs`
|
||||
- `docs/qa-surface-reflection/baseline-mosaic.frag` (pre-edit snapshot)
|
||||
- `docs/qa-surface-reflection/implementation-report.md`
|
||||
|
||||
The existing rectangular reflection remains intact and is now named
|
||||
`specGloss`. A separate broad reflection is added after color-plane
|
||||
simplification and before `#include <colorspace_fragment>`.
|
||||
|
||||
Final coefficients and rationale:
|
||||
|
||||
- Surface reflection edge weight: `mix(0.35, 1.0, edgeWeight)`. This keeps the
|
||||
existing refraction `edgeWeight` and `strength` path unchanged while reducing
|
||||
reflection at pane edges without removing it entirely.
|
||||
- Existing rectangular reflection strength: `.12`, unchanged.
|
||||
- Broad reflection strength: `.035`, intentionally low relative to the
|
||||
rectangular lobe.
|
||||
- Broad spatial footprint: `smoothstep(vec2(.15), vec2(1.75), windowCoord)`,
|
||||
wider and softer than the existing `.45` to `1.15` rectangular mask.
|
||||
- View/surface response: `pow(max(dot(nCurved, v), 0.), 2.)`, using the same
|
||||
signed curved normal that drives refraction and the existing reflection.
|
||||
- Both reflection amplitudes use the same `clamp(uGlossStrength, 0., 1.)`
|
||||
gate, so gloss `0` makes both contributions exactly zero.
|
||||
|
||||
No environment texture, render pass, render target, dependency, palette input,
|
||||
history input, UI, camera, placement, framing, or other glass-mode change was
|
||||
added.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
Focused regression: `scripts/test-surface-reflection.mjs`.
|
||||
The assertions are source/math-only and do not claim to execute GLSL or prove
|
||||
browser visuals.
|
||||
|
||||
### RED
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
node scripts/test-surface-reflection.mjs
|
||||
```
|
||||
|
||||
Exact result against the pre-edit shader:
|
||||
|
||||
```text
|
||||
exit_code=1
|
||||
node:internal/modules/run_main:107
|
||||
triggerUncaughtException(
|
||||
^
|
||||
|
||||
AssertionError [ERR_ASSERTION]: both surface reflection terms must share a clamped gloss gate
|
||||
at file:///D:/project/behind_glass/scripts/test-surface-reflection.mjs:11:8 {
|
||||
generatedMessage: false,
|
||||
actual: '#ifndef REGION_ATLAS\n' +
|
||||
' color.a=1.;\n' +
|
||||
' 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;\n' +
|
||||
' #include <colorspace_fragment>\n',
|
||||
expected: /float glossStrength=clamp\(uGlossStrength,0\.,1\.\);/,
|
||||
operator: 'match'
|
||||
}
|
||||
```
|
||||
|
||||
This failed for the intended reason: the pre-edit shader had one reflection
|
||||
term, no shared gloss gate, no broad reflection, and no separate surface edge
|
||||
weight.
|
||||
|
||||
### GREEN
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
node scripts/test-surface-reflection.mjs
|
||||
```
|
||||
|
||||
Exact result after the minimal shader edit:
|
||||
|
||||
```text
|
||||
exit_code=0
|
||||
surface reflection source/math regression passed
|
||||
```
|
||||
|
||||
The final regression checks zero-gloss gating, `.35` to `1.0` surface-weight
|
||||
bounds, preservation of the refraction edge weight, normal/view and light-hit
|
||||
spatial dependence, non-atlas placement, and ordering before color-space
|
||||
conversion.
|
||||
|
||||
## Automated verification
|
||||
|
||||
All six existing scripts from `docs/qa-window-immersion/verification.md` were
|
||||
run with `node --experimental-strip-types` after implementation.
|
||||
|
||||
```text
|
||||
=== node --experimental-strip-types scripts/test-camera-history.mjs ===
|
||||
Camera/history checks passed: default/max clamp, model reset keys, ping-pong and disposal, premultiplied color, cadence 0.999955/0.999955/0.999955, 70ms settling.
|
||||
=== node --experimental-strip-types scripts/test-region-axis-split.mjs ===
|
||||
Region axis-split checks passed: SSE axis/cut/pair scoring, vertical/horizontal cuts 2/6, observed side-label pairs, checkerboard model, single-label fallback, metadata round-trip, confidence blending, dark eye/nose protection, and edge-independent color strength.
|
||||
=== node --experimental-strip-types scripts/test-mosaic-depth-distance.mjs ===
|
||||
Mosaic depth-region projection checks passed: 21 perspective reconstructions, 12 original-depth UV mappings across camera/zoom-pan/depthScale, near-depth gate, distance-independent color application, and invalid-surface guards.
|
||||
=== node --experimental-strip-types scripts/test-mosaic-continuity.mjs ===
|
||||
Mosaic projected-UV continuity passed: 493760 Jacobians, 87200 seam convergence checks, center/edge anchors, and view-dependent within-cell displacement.
|
||||
=== node --experimental-strip-types scripts/test-region-atlas-dark-sampling.mjs ===
|
||||
Region atlas dark sampling source/math-only regression passed: phase-offset narrow feature, far/invalid/uniform invariants, and four-probe bounded scoring contract.
|
||||
=== node --experimental-strip-types scripts/test-window-reveal-coverage.mjs ===
|
||||
Window reveal coverage passed: 146 previous-frame misses sealed across 2 aspects, 3 depth scales, 17 view offsets (including neutral) and 49 aperture samples; geometry reuse/disposal verified.
|
||||
```
|
||||
|
||||
Build command:
|
||||
|
||||
```text
|
||||
npm.cmd run build
|
||||
```
|
||||
|
||||
Result: exit code `0`; TypeScript and Vite production build succeeded. Vite
|
||||
reported the existing large-chunk advisory for the main and AI-depth assets.
|
||||
|
||||
## Visual comparison and limitations
|
||||
|
||||
No browser or GPU visual capture was performed in this implementation task.
|
||||
The preserved shader baseline is available at
|
||||
`docs/qa-surface-reflection/baseline-mosaic.frag`; use it for the
|
||||
same-viewport dog1 comparison with the brief's fixed values, including gloss
|
||||
0, 0.30, and 0.40, curvature -1/0/+1, camera 0.90, portrait, and indoor
|
||||
checks.
|
||||
|
||||
The automated regression does not prove grass highlight appearance, adjacent
|
||||
cell coherence, edge leakage, eye/nose readability, mobile behavior, or GPU
|
||||
shader compilation. Those remain visual/browser verification items. No claim
|
||||
of GPU, mobile, or visual success is made here.
|
||||
|
||||
## Round 1/5 regression hardening
|
||||
|
||||
Addressed both Astra findings without changing production scope. The focused
|
||||
script now extracts individual GLSL statements instead of using unrestricted
|
||||
cross-statement matching, compares `edgeWeight`, `strength`, and `sceneHit`
|
||||
against the preserved pre-edit shader, inspects the effective atlas branch,
|
||||
and verifies simplification before final composition and composition before
|
||||
color-space conversion. It evaluates the extracted `specGloss`,
|
||||
`broadReflection`, and final mix expressions for gloss-zero output plus
|
||||
representative spatial/view variation. It also applies an in-memory constant
|
||||
50% final-mix mutation and requires the covering assertions to reject it.
|
||||
|
||||
Exact covering command:
|
||||
|
||||
```text
|
||||
node scripts/test-surface-reflection.mjs
|
||||
```
|
||||
|
||||
Exact output:
|
||||
|
||||
```text
|
||||
exit_code=0
|
||||
surface reflection source/math regression passed; constant-output mutation rejected
|
||||
```
|
||||
|
||||
Only the focused regression was rerun because this round changed no
|
||||
production file; the six existing checks and build remain covered by the prior
|
||||
verification record above.
|
||||
|
||||
## Round 2/5 atlas exclusion hardening
|
||||
|
||||
Addressed the remaining atlas-exclusion finding without changing production
|
||||
code. `scripts/test-surface-reflection.mjs` now walks all `REGION_ATLAS`
|
||||
preprocessor branches, retains the complete effective source for the macro
|
||||
defined, and compares that source with the preserved baseline. It also inserts
|
||||
an in-memory gloss-dependent white reflection immediately after the earlier
|
||||
atlas `atlasCenterColor` sample and requires the covering assertions to reject
|
||||
that mutation. This remains a source/math regression and is not GPU visual
|
||||
verification.
|
||||
|
||||
Exact covering command:
|
||||
|
||||
```text
|
||||
node scripts/test-surface-reflection.mjs
|
||||
```
|
||||
|
||||
Exact output:
|
||||
|
||||
```text
|
||||
exit_code=0
|
||||
surface reflection source/math regression passed; constant-output and atlas-reflection mutations rejected
|
||||
```
|
||||
|
||||
No full suite or build was rerun because the production shader remained
|
||||
unchanged.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Surface reflection browser verification
|
||||
|
||||
2026-09-23, local Vite preview at 1280×720 unless noted. The primary comparison used dog1 after the same cached AI relative-depth generation completed: Final / Mosaic / 조각 유리, Grid 13, curvature +1, gloss 0.30, light 0.60/0.80, depth scale 0.25, camera offset 0.50, zoom/pan 1/0/0.
|
||||
|
||||
## Same-condition comparison
|
||||
|
||||
- `before-default.png`: pre-edit shader, centered view.
|
||||
- `after-default.png`: final shader, centered view.
|
||||
- `before-right.png`, `after-right.png`: equivalent pointer-drag probe; these are qualitative because camera smoothing does not guarantee identical final coordinates.
|
||||
- The centered glass-pane crop changed by a mean absolute `0.3106/255` per RGB channel, maximum `14`, over `13.85%` of channels. The top-right quadrant contained nearly all of the visible change (`1.2244/255` mean absolute difference); the other three quadrants were below `0.013/255`.
|
||||
- Compared with gloss 0, the pre-edit default gloss affected `12.80%` of crop channels with `0.6569/255` mean absolute difference; the final default gloss affected `15.81%` with `0.9015/255`. This is a broader but deliberately subtle highlight, not uniform pane brightening.
|
||||
|
||||
The dog’s two dark eye planes and nose plane remained visible. Grass and face were not visibly washed out. The centered difference is too subtle to prove visual improvement by metric alone; `after-glass-only.png` confirms that the widened reflection remains localized around the common top-right light rather than becoming a constant white overlay.
|
||||
|
||||
## Preservation checks
|
||||
|
||||
- `before-gloss0.png` and `after-gloss0.png` are pixel-identical in the pane crop: mean absolute difference `0`, maximum `0`, changed channels `0`.
|
||||
- `after-gloss040.png`: no obvious face/grass washout at gloss 0.40.
|
||||
- `after-curvature0.png`, `after-curvature-minus1.png`: flat and concave cases rendered without reflection spill; the dark facial features remained identifiable. Curvature +1 was the default comparison.
|
||||
- `after-camera090-right.png`: camera range 0.90 plus horizontal drag showed no reflection outside the pane, bright rim, or exposed background gap.
|
||||
- `after-portrait.png`: 390×844 viewport retained the frame/pane fit and dark eye/nose planes. The temporary viewport override was reset.
|
||||
- `after-indoor.png`: dog2 with completed AI depth rendered without reflection washout or pane-edge leakage.
|
||||
- Reeded and Frosted were selected and rendered, then Mosaic was restored. No runtime errors appeared.
|
||||
|
||||
The console contained only the already documented Three.js shader warning `X3595` about gradient instructions in a varying loop. No new error was observed.
|
||||
|
||||
## Final fresh verification
|
||||
|
||||
After both regression-review fix rounds, the controller reran the current workspace state. `scripts/test-surface-reflection.mjs` and the six existing regression scripts all exited successfully. `npm.cmd run build` completed TypeScript and Vite production output successfully; the existing main-chunk size advisory remained.
|
||||
|
||||
## Limits
|
||||
|
||||
- The requested intermediate “broad reflection only, before edge-weight separation” GPU screenshot was not captured; the exact pre-edit and final states are preserved instead.
|
||||
- The pointer-drag screenshots do not record an exact camera coordinate and therefore are qualitative.
|
||||
- Continuous-video flicker, physical mobile GPU performance, and real-device touch/gyro were not measured.
|
||||
- The browser comparison establishes no obvious regression and a small widened reflection footprint. Whether the subtle default-strength change is aesthetically strong enough remains a product judgment, not an automated pass condition.
|
||||