跳转至

探索 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 中有非常强大的 dFdxdFdy 函数,它们可以计算屏幕坐标中任何 varying 变量的变化率。在这里,我们计算纹理坐标的变化率,并再次用纹理尺寸进行缩放,以便得到每像素的变化。将梯度转换到屏幕坐标后,我们将其用作 smoothstep 函数的上限阈值,这为我们提供了阶跃函数的三次近似。smoothstep 测试的值是插值值与等值线值之间差异的绝对值,其输出用作 baseColor 值与亮红色(等值线的颜色)之间的插值因子。

当从上方观察曲面时,等值线看起来完美无缺,但在倾斜角度下,您可以看到垂直面上等值线的一些拖影伪影。这很可能是由于 dFdxdFdy 函数计算方式造成的。它们作用于屏幕像素的 2x2 邻域来计算有限差分,因此无法保证给定邻域中的所有四个像素都来自同一个三角形,甚至在发生自遮挡时都来自曲面的同一部分。所以 Jacobian 矩阵是一个近似值,而不是偏导数的精确表示,因此我们得到了这些伪影。但是,这种方法的复杂度基于屏幕上的像素数量,而不是被渲染的数据点或三角形的数量,因此在探索新数据时,它可以用于快速评估等值线。

松岛冰川发生了什么? 关注即将到来的 SAR 传感器