探索 IDLgrShader 的乐趣
原文链接: https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/fun-with-idlgrshader
19246 为本文评分:
尚无评分
探索 IDLgrShader 的乐趣
作者: 匿名 发表日期: 2014年5月8日,星期四
在我之前的工作中,我做了很多 OpenGL 编程,尤其是使用 GLSL 着色器。因此,我很想了解 IDL 的 IDLgrShader 类有哪些功能,以及如何将其与其他图形对象一起使用。本文讨论了我构建的一个着色器原型,它可以在一个曲面上可视化等值线,并且可以非常快速地进行移动或动画显示,而无需重新计算其几何形状。
让我们从加载示例数据文件、构建用于曲面可视化的基本场景图以及自定义着色器的 PRO 代码开始:
function pow2Ceil, x
Pow2 = IShft(1, Lindgen(16))
w = Where(x and Pow2, count)
return, (count eq 1) ? Pow2[w[0]] : Pow2[w[-1]+1]
end
pro SurfaceIsoContour
; load Maroon Bells binary data, using dimensions and data type from index.txt
m = 350
n = 450
heights = Float(Read_Binary(FilePath('surface.dat', ROOT=!Dir, $
SUBDIR=['examples', 'data']), $
DATA_TYPE=2, DATA_DIMS=[m, n]))
; scale height data to make commensurate with horizontal scale
heights *= 0.1
minHeight = Min(heights, MAX=maxHeight)
meanHeight = (minHeight + maxHeight) * 0.5
; Basic scene graph
oWin = IDLitWindow(DIMENSIONS=[m, n])
oView = IDLgrView(COLOR=[150,150,255], VIEWPLANE_RECT=[-m*0.5, -n*0.5, m, n], $
ZCLIP=[n*0.5, -n*0.5], EYE=n)
oWin.SetProperty, GRAPHICS_TREE=oView
; build model and add to view
oModel = IDLgrModel()
oView->Add, oModel
; create surface and add to model
oSurf = IDLgrSurface(DATAX=Findgen(m), DATAY=Findgen(n), $
DATAZ=heights, STYLE=2, USE_TRIANGLES=1)
oModel->Add, oSurf
; create image to use as texture in fragment shader
oHeightImage = IDLgrImage(DATA=heights, INTERNAL_DATA_TYPE=3, INTERPOLATE=0)
textureSize = [1.0*pow2Ceil(m), 1.0*pow2Ceil(n)]
; create shader
oShader = Obj_New('IDLgrShader')
baseDir = File_DirName((Scope_TraceBack(/STRUCTURE))[-1].filename) + Path_Sep()
vertFile = baseDir + 'HeightMapShader.vert'
fragFile = baseDir + 'SurfaceIsocontourShader.frag'
oShader->SetProperty, VERTEX_PROGRAM_FILENAME=vertFile, $
FRAGMENT_PROGRAM_FILENAME=fragFile
oShader->SetUniformVariable, 'heightMap', oHeightImage
oShader->SetUniformVariable, 'uImageDim', [textureSize, 1.0/textureSize]
oShader->SetUniformVariable, 'uContourValue', 0.0
oShader->SetUniformVariable, 'uHeightOffsetScale', [-Min(heights), $
1.0/(Max(heights) - Min(heights))]
oSurf->SetProperty, SHADER=oShader, TEXTURE_MAP=oHeightImage
; create mouse event observer
oObs = Obj_New('surfaceObserver')
oObs->SetProperty, MODEL=oModel, SHADER=oShader, $
ORIGIN=[m/2.0, n/2.0, meanHeight], $
DATA_RANGE=maxHeight-minHeight, DATA_MEAN=meanHeight
oWin->SetEventMask, /TIMER_EVENTS, /BUTTON_EVENTS, /MOTION_EVENTS
oWin->AddWindowEventObserver, oObs
oWin->SetTimerInterval, 1.0/30.0
end
SurfaceIsoContour 函数加载了示例数据文件 surface.dat,其中包含规则网格中的一块 Maroon Bells 地形。高程数据按 0.1 的比例因子缩小,以便垂直比例与水平比例相当。然后,我们构建一个基本的 IDLitWindow 对象来容纳一个基本场景图,其中包含一个模型,该模型有一个曲面。IDLgrView 被配置为创建曲面的正交视图,尽管在某些方位角旋转时会发生轻微的裁剪。曲面使用 Findgen 数组构造 x 和 y 坐标,使其成为一个规则网格。STYLE 属性设置为 2 表示填充曲面,不过这个原型同样适用于线框模式 (1)、RuledXZ (3) 和 RuledYZ (4) 模式。无论 USE_TRIANGLES 设置为 0(四边形)还是 1(三角形),它都能很好地工作。高程数组也被加载到一个 IDLgrImage 对象中,该对象将作为纹理传递给着色器。着色器的初始化和配置将在下面显示源代码后讨论。
SurfaceObserver 类是一个非常基本的鼠标事件处理器,它只允许用鼠标左键绕原点旋转。它还有一个定时器回调函数,该函数以正弦波模式在高程范围内更新等值线的值。定时器设置为 30 Hz,因此等值线循环重复大约需要 12 秒。
function SurfaceObserver::init
self.elevation = 90.0
return, 1
end
pro SurfaceObserver::cleanup
end
pro SurfaceObserver::SetProperty, MODEL=model, SHADER=shader, ORIGIN=origin, $
DATA_RANGE=dataRange, DATA_MEAN=dataMean
if (N_ELEMENTS(model) gt 0) then self.oModel = model
if (N_ELEMENTS(shader) gt 0) then self.oShader = shader
if (N_ELEMENTS(dataRange) gt 0) then self.dataRange = dataRange
if (N_ELEMENTS(dataMean) gt 0) then self.dataMean = dataMean
if (N_ELEMENTS(origin) gt 0) then begin
self.origin = origin
self->UpdateEye
endif
end
pro SurfaceObserver::OnMouseDown, oWin, x, y, ButtonMask, Modifiers, NumClick
self.mouseDown = ButtonMask
self.lastMousePos = [x, y]
end
pro SurfaceObserver::OnMouseMotion, oWin, x, y, Modifiers
if ((self.mouseDown AND 1) gt 0) then begin
delta = [x, y] - self.lastMousePos
self.lastMousePos = [x, y]
self.azimuth += delta[0]
self.elevation += delta[1]
self->UpdateEye
oWin->Draw
endif
end
pro SurfaceObserver::OnMouseUp, oWin, x, y, ButtonMask
self.mouseDown = 0
end
pro SurfaceObserver::OnTimer, oWin
self.time = (self.time + 1) mod 360
contourVal = self.dataMean + 0.5 * self.dataRange * sin(self.time * !DTOR)
self.oShader->SetUniformVariable, 'uContourVal', contourVal
oWin->Draw
end
pro SurfaceObserver::UpdateEye
self.oModel->Reset
self.oModel->Translate, -self.origin[0], -self.origin[1], -self.origin[2]
self.oModel->Rotate, [0, 0, 1], self.azimuth
self.oModel->Rotate, [1, 0, 0], 90.0 - self.elevation
end
pro SurfaceObserver__define
void = { SurfaceObserver, $
oModel: Obj_New(), $
oShader: Obj_New(), $
mouseDown: 0L, $
lastMousePos: [0L, 0L], $
origin: [0.0, 0.0, 0.0], $
azimuth: 0.0, $
elevation: 0.0, $
time: 0L, $
dataRange: 0.0, $
dataMean: 0.0 $
}
end
现在让我们来看看着色器的源代码,因为魔法就在这里发生。请记住,IDLgrShader 类使用 OpenGL 2.1 API,因此它拥有所有内置的 uniform 变量,如 gl_ModelViewProjectionMatrix。顶点着色器是一个非常基本的直通着色器,将输入的顶点坐标变换到归一化设备坐标,并复制纹理坐标(这对片元着色器至关重要)。顶点着色器存储在名为 HeightMapShader.vert 的文件中,与主 PRO 文件位于同一文件夹。
void main(void)
{
// get the height value from the texture
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_TexCoord[0] = gl_MultiTexCoord0;
}
主要工作由片元着色器完成,该着色器存储在名为 SurfaceIsocontour.frag 的文件中,与主 PRO 文件位于同一文件夹。此着色器定义了四个 uniform 变量:
heightMap- 这是一个作为纹理的高度图,由 IDLgrImage 类为我们编排。由于我们使用的是 OpenGL 2.0 和 sampler2D 接口,纹理在两个维度上都必须是 2 的幂,并且采样坐标在 [0, 1] 范围内。纹理填充由 IDLgrImage 自动处理。uImageDim- 这是一个 4 元素数组[texture width, texture height, 1/texture width, 1/texture height]。由于纹理在两个维度上都必须是 2 的幂,我们不能使用高度数组的实际尺寸,需要将尺寸向上舍入到下一个 2 的幂,这就是pow2Ceil函数为我们做的。uHeightOffsetScale- 这是一个 2 元素数组[-min height value, 1/height range],用于根据片元插值的高程值为其计算平滑的黑->白颜色渐变。uContourVal- 这是绘制等值线的高程值。
uniform sampler2D heightMap;
uniform vec4 uImageDim;
uniform vec2 uHeightOffsetScale;
uniform float uContourVal;
void main(void)
{
// Get the fractional texel coordinate for this fragment, offset by 0.5
vec2 fractST = fract(gl_TexCoord[0].st * uImageDim.st) - vec2(0.5);
vec2 dsdt = sign(fractST) * uImageDim.zw;
// use the dsdt values, which are +1 or -1, to sample four closest neighbor texels
float texVal1 = texture2D(heightMap, gl_TexCoord[0].st).r;
float texVal2 = texture2D(heightMap, gl_TexCoord[0].st + vec2(dsdt.s, 0.0)).r;
float texVal3 = texture2D(heightMap, gl_TexCoord[0].st + vec2(0.0, dsdt.t)).r;
float texVal4 = texture2D(heightMap, gl_TexCoord[0].st + dsdt).r;
// use the fractional texel coordinate values to linearly interpolate all four edges
float ave12 = mix(texVal1, texVal2, abs(fractST.s));
float ave34 = mix(texVal3, texVal4, abs(fractST.s));
float ave13 = mix(texVal1, texVal3, abs(fractST.t));
float ave24 = mix(texVal2, texVal4, abs(fractST.t));
// bilinearly interpolate fragment's value
float myValue = mix(ave12, ave34, abs(fractST.t));
// use minVal/maxVal uniforms to perform linear bytescale of fragment value
vec3 baseColor = mix(vec3(0.0), vec3(1.0), (myValue + uHeightOffsetScale.x) * uHeightOffsetScale.y);
// construct texture gradient vector
vec2 valGrad = vec2(ave24 - ave13, ave34 - ave12);
// use derivative operators to construct Jacobian matrix, scaled by image size
vec2 xgrad = uImageDim.st * dFdx(gl_TexCoord[0].st);
vec2 ygrad = uImageDim.st * dFdy(gl_TexCoord[0].st);
// mat2 constructor is column major, so xgrad and ygrad are its columns
mat2 jacob = mat2(xgrad, ygrad);
// transform texture gradient by Jacobian into screen space
valGrad = jacob * valGrad;
// use gradient length as line width to perform antialiased blend of line color with height color ramp
float dist = abs(myValue - uContourVal);
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(baseColor, 1.0), smoothstep(0.0, length(valGrad), dist));
}
着色器首先要做的是确定出射片元位于源纹素的哪个象限。它获取片元的纹理坐标,然后乘以纹理尺寸,这样每个纹素占据 [m, m+1) x [n, n+1),中心在 (m+0.5, n+0.5)。调用 fract() 函数会去掉整数偏移,将其限制在 [0, 1) x [0, 1) 范围内。然后在两个维度上都减去 0.5,这样我们就可以使用 sign() 函数来识别纹素象限。接着将其乘以每个维度上一个像素的大小,这样我们就得到了获取邻近像素所需的偏移量,以便进行双线性插值和梯度估计。
然后,它使用这些偏移量获取片元周围的 2x2 邻域像素。然后使用 mix() 函数对这些像素进行成对插值,使用纹素象限坐标作为插值权重。然后,在垂直方向对顶部和底部边缘进行插值,得到片元的双线性插值值。虽然我们可以通过将 IDLgrImage 的 INTERPOLATION 属性设置为 1 来自动完成此操作,但我们仍然需要这些线性插值的边缘值来近似表面梯度。
我们使用片元的插值高程值来计算颜色渐变,该渐变存储在 baseColor 变量中。接下来是线性代数课程,我们构建表面梯度,该梯度位于模型的坐标系中。我们需要将其转换到屏幕坐标,因此我们构建了 Jacobian 矩阵来完成此操作。GLSL 中有非常强大的 dFdx 和 dFdy 函数,它们可以计算屏幕坐标中任何 varying 变量的变化率。在这里,我们计算纹理坐标的变化率,并再次用纹理尺寸进行缩放,以便得到每像素的变化。将梯度转换到屏幕坐标后,我们将其用作 smoothstep 函数的上限阈值,这为我们提供了阶跃函数的三次近似。smoothstep 测试的值是插值值与等值线值之间差异的绝对值,其输出用作 baseColor 值与亮红色(等值线的颜色)之间的插值因子。
当从上方观察曲面时,等值线看起来完美无缺,但在倾斜角度下,您可以看到垂直面上等值线的一些拖影伪影。这很可能是由于 dFdx 和 dFdy 函数计算方式造成的。它们作用于屏幕像素的 2x2 邻域来计算有限差分,因此无法保证给定邻域中的所有四个像素都来自同一个三角形,甚至在发生自遮挡时都来自曲面的同一部分。所以 Jacobian 矩阵是一个近似值,而不是偏导数的精确表示,因此我们得到了这些伪影。但是,这种方法的复杂度基于屏幕上的像素数量,而不是被渲染的数据点或三角形的数量,因此在探索新数据时,它可以用于快速评估等值线。