From e19eb1933f95ade4c6028fe4f5712a534e0b4d3b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 01:07:21 +0000 Subject: [PATCH] webgl: make pick and depth shaders honor bumpy_flatmap (gh-704) The pick and depth shaders hardcoded the thickness-based cortical-sheet displacement (.62 * thickness) and knew nothing about HASFLAT or the bumpyflat uniform, so on any subject with a flatmap they tested against geometry that isn't the geometry being drawn: bumpy_flatmap on -> surface bumps by flatheight, pick/depth used .62*thickness bumpy_flatmap off -> surface isn't displaced at all, pick/depth still displaced The second row is the worse one, and it isn't about bumpy flatmaps: on a partially unfolded surface with the bump off, the picker pushed vertices 0.62 * thickness along their normals while the surface stayed put, which shows up as picking that lands a few pixels off (worst near silhouette edges, where the normal is perpendicular to the view direction) and as ROI labels occluded against slightly the wrong surface. Rather than add a fourth copy of the displacement expression -- four copies is how the shaders drifted apart to begin with -- pull it into utils.cortsheet_displace and have all four shaders emit it, along with utils.bumpyflat for the uniform and attribute it needs. pick and depth now take a hasflat opt, bind the flatheight attribute, and share the surface's bumpyflat uniform object so they follow the toggle without listening for it. The GLSL the surface shaders emit is unchanged apart from where the flatheight declaration sits. Fixes #704 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Tr6MczRdE3ad5ss4FcsRcV --- cortex/webgl/resources/js/facepick.js | 11 +++- cortex/webgl/resources/js/shaderlib.js | 79 ++++++++++++++++--------- cortex/webgl/resources/js/svgoverlay.js | 6 ++ 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/cortex/webgl/resources/js/facepick.js b/cortex/webgl/resources/js/facepick.js index 0747ff576..4ec7d8d1d 100644 --- a/cortex/webgl/resources/js/facepick.js +++ b/cortex/webgl/resources/js/facepick.js @@ -46,9 +46,18 @@ function PickPosition(surf, posdata) { max: { type:'v3', value:this.bounds.max}, thickmix:surf.uniforms.thickmix, surfmix:surf.uniforms.surfmix, + //Shared object, not a copy, so the picker follows the bumpy flatmap + //toggle without having to listen for it. + bumpyflat:surf.uniforms.bumpyflat, }; - var shaders = Shaders.pick({morphs:surf.names.length, volume:surf.volume}); + //hasflat has to match what the surface shaders were built with, otherwise + //the picker tests against geometry that isn't the one being drawn. + var shaders = Shaders.pick({ + morphs:surf.names.length, + volume:surf.volume, + hasflat:surf.flatlims !== undefined, + }); this.shade_x = new THREE.ShaderMaterial({ vertexShader: shaders.vertex, fragmentShader: shaders.fragment[0], diff --git a/cortex/webgl/resources/js/shaderlib.js b/cortex/webgl/resources/js/shaderlib.js index dc4deb82e..4c918666d 100644 --- a/cortex/webgl/resources/js/shaderlib.js +++ b/cortex/webgl/resources/js/shaderlib.js @@ -192,6 +192,38 @@ var Shaderlib = (function() { "float use_thickmix = thickmix;", "#endif", ].join("\n"), + + // bumpyflat: header code declaring what cortsheet_displace needs beyond + // the plain surface attributes. Required by every vertex shader that + // displaces the cortical sheet, whether or not it goes on to shade it. + bumpyflat: [ + "uniform int bumpyflat;", + "float f_bumpyflat = float(bumpyflat);", + "#ifdef HASFLAT", + "attribute float flatheight;", + "#endif", + ].join("\n"), + + // cortsheet_displace: pushes the mixed surface position `pos` off the + // cortical sheet along `norm`. Every shader that needs to agree on + // where the surface actually is has to apply the identical expression: + // pick and depth test against geometry that must match what is drawn, + // and they drifted away from the surface shaders precisely because + // each shader carried its own copy of this (gh-704). + // `thickmix` names the mixing variable in the calling shader; shaders + // that use utils.thickmixer_main get the default. + cortsheet_displace: function(morphs, thickmix) { + thickmix = thickmix || "use_thickmix"; + return [ + "#ifdef CORTSHEET", + "#ifdef HASFLAT", + "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * mix(1., 0., "+thickmix+") * flatheight * f_bumpyflat;", + "#else", + "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * .62 * distance(position, wm.xyz) * mix(1., 0., "+thickmix+");", + "#endif", + "#endif", + ].join("\n"); + }, } var module = function() { @@ -360,8 +392,7 @@ var Shaderlib = (function() { "uniform mat4 volxfm[2];", // "uniform float thickmix;", utils.thickmixer, - "uniform int bumpyflat;", - "float f_bumpyflat = float(bumpyflat);", + utils.bumpyflat, "attribute vec4 wm;", "attribute vec3 wmnorm;", @@ -369,7 +400,6 @@ var Shaderlib = (function() { "#ifdef HASFLAT", "attribute vec3 flatBumpNorms;", - "attribute float flatheight;", "#endif", // "attribute float dropout;", @@ -428,14 +458,7 @@ var Shaderlib = (function() { // "norm = mix(flatBumpNorms, normalize(onorm), thickmix);", // "norm = normalize(flatBumpNorms);", - "#ifdef CORTSHEET", - // - "#ifdef HASFLAT", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * mix(1., 0., use_thickmix) * flatheight * f_bumpyflat;", - "#else", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * .62 * distance(position, wm.xyz) * mix(1., 0., use_thickmix);", - "#endif", - "#endif", + utils.cortsheet_displace(morphs), "#ifdef HASFLAT", "vNormal = normalMatrix * mix(norm, flatBumpNorms, (1.0 - use_thickmix) * clamp(surfmix*"+(morphs-1)+". - 1.0, 0., 1.) * f_bumpyflat);", @@ -710,8 +733,7 @@ var Shaderlib = (function() { "uniform float framemix;", // "uniform float thickmix;", utils.thickmixer, - "uniform int bumpyflat;", - "float f_bumpyflat = float(bumpyflat);", + utils.bumpyflat, "varying vec4 vColor;", "#ifdef RGBCOLORS", @@ -731,7 +753,6 @@ var Shaderlib = (function() { "#ifdef HASFLAT", "attribute vec3 flatBumpNorms;", - "attribute float flatheight;", "#endif", // "attribute float dropout;", @@ -786,13 +807,7 @@ var Shaderlib = (function() { "vec3 pos, norm;", "mixfunc(mpos, mnorm, pos, norm);", - "#ifdef CORTSHEET", - "#ifdef HASFLAT", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * mix(1., 0., use_thickmix) * flatheight * f_bumpyflat;", - "#else", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * .62 * distance(position, wm.xyz) * mix(1., 0., use_thickmix);", - "#endif", - "#endif", + utils.cortsheet_displace(morphs), "#ifdef HASFLAT", "vNormal = normalMatrix * mix(norm, flatBumpNorms, (1.0 - use_thickmix) * clamp(surfmix*"+(morphs-1)+". - 1.0, 0., 1.) * f_bumpyflat);", @@ -944,10 +959,13 @@ var Shaderlib = (function() { var morphs = opts.morphs; if (opts.volume > 0) header += "#define CORTSHEET\n"; + if (opts.hasflat) + header += "#define HASFLAT\n"; var vertShade = [ // "uniform float thickmix;", utils.thickmixer, + utils.bumpyflat, utils.mixer(morphs), "attribute vec4 wm;", @@ -972,9 +990,7 @@ var Shaderlib = (function() { "vec3 pos, norm;", "mixfunc(mpos, mnorm, pos, norm);", - "#ifdef CORTSHEET", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * .62 * distance(position, wm.xyz) * mix(1., 0., use_thickmix);", - "#endif", + utils.cortsheet_displace(morphs), "gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );", "}", @@ -1004,6 +1020,10 @@ var Shaderlib = (function() { auxdat: { type: 'v4', value:null }, }; + if (opts.hasflat) { + attributes.flatheight = { type: 'f', value:null }; + } + for (var i = 0; i < morphs-1; i++) { attributes['mixSurfs'+i] = { type:'v4', value:null}; attributes['mixNorms'+i] = { type:'v3', value:null}; @@ -1017,9 +1037,12 @@ var Shaderlib = (function() { var morphs = opts.morphs; if (opts.volume > 0) header += "#define CORTSHEET\n"; + if (opts.hasflat) + header += "#define HASFLAT\n"; var vertShade = [ "uniform float thickmix;", + utils.bumpyflat, utils.mixer(morphs), "attribute vec4 wm;", @@ -1037,9 +1060,7 @@ var Shaderlib = (function() { "vec3 pos, norm;", "mixfunc(mpos, mnorm, pos, norm);", - "#ifdef CORTSHEET", - "pos += clamp(surfmix*"+(morphs-1)+"., 0., 1.) * normalize(norm) * .62 * distance(position, wm.xyz) * mix(1., 0., thickmix);", - "#endif", + utils.cortsheet_displace(morphs, "thickmix"), "gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );", "}", @@ -1060,6 +1081,10 @@ var Shaderlib = (function() { wmnorm: { type: 'v3', value:null }, }; + if (opts.hasflat) { + attributes.flatheight = { type: 'f', value:null }; + } + for (var i = 0; i < morphs-1; i++) { attributes['mixSurfs'+i] = { type:'v4', value:null}; attributes['mixNorms'+i] = { type:'v3', value:null}; diff --git a/cortex/webgl/resources/js/svgoverlay.js b/cortex/webgl/resources/js/svgoverlay.js index 71c1a1b48..f445763c4 100644 --- a/cortex/webgl/resources/js/svgoverlay.js +++ b/cortex/webgl/resources/js/svgoverlay.js @@ -39,9 +39,12 @@ var svgoverlay = (function(module) { this.layers = {}; this.surf = surf; + //hasflat has to match what the surface shaders were built with, otherwise + //labels get occluded against geometry that isn't the one being drawn. var shader = Shaders.depth({ morphs:surf.names.length, volume:surf.volume, + hasflat:surf.flatlims !== undefined, }); this.depthshade = new THREE.ShaderMaterial({ vertexShader: shader.vertex, @@ -49,6 +52,9 @@ var svgoverlay = (function(module) { uniforms: { thickmix:surf.uniforms.thickmix, surfmix:surf.uniforms.surfmix, + //Shared object, not a copy, so the depth pass follows the + //bumpy flatmap toggle without having to listen for it. + bumpyflat:surf.uniforms.bumpyflat, }, attributes: shader.attrs, blending: THREE.CustomBlending,