【问题标题】:Calculating point to line distance in GLSL在GLSL中计算点到线的距离
【发布时间】:2020-12-08 23:26:57
【问题描述】:

我正在尝试计算 GSLS 中的点到线距离 - 正是在 turbo.js turbo.js

这是一个更普遍的问题的一部分,我尝试找到与一组 GeoJSON 点相对应的 [GeoJSON 多线上最近的点] - 在 1000 段线上设置的 500 点的计算数量最终是500k 点到距离计算。

这在浏览器中(甚至在工作线程中)处理太多了,所以并行性有很大帮助。

诀窍在于,AFAIK 我只能使用 vec4 作为输入,这意味着我只能对点对进行计算。

到目前为止,我已经开始计算所有线对的距离和方位 - 但无法在最后一步计算点到线的距离。

所以问题是 - 给出 3 点 a、b 和 c,并且知道

  • 他们在经纬度的位置
  • 它们的成对方位角和距离

是否可以使用使用 vec2、vec3 或 vec4 作为输入参数的变换计算从 a 到 b 和 c 定义的线的距离

作为一个子问题 - 如果三角形 (a, b, c) 的高度不与线 (a, b) 相交,我知道如何计算距离,因为它是 min(distance(a, b) , 距离(a, c))。

但是,如果它相交,我该如何计算呢?

【问题讨论】:

  • @LJᛃ - 不,它没有,因为问题是当函数的最大输入参数数量为 4 时如何处理它(read()x 提取的 vec4 )。
  • 嗯好吧,这不是webglglsl 的问题。由于该库没有标签,因此我会说您最好的选择是在该库的 github 上提出问题。

标签: geometry glsl webgl


【解决方案1】:

我不确定我是否理解你的问题。

听起来你想知道 500 个输入点,对于 1000 个线段,对于每个点,哪个线段最接近。

如果这就是您要问的,那么将所有点放在浮点纹理中(纹理的另一个词是 2D 数组)。绘制一个 -1 到 +1 的四边形,它是结果数量的大小(500 个结果,因此 50x10 或 25x20 等。)传入纹理的分辨率。使用gl_FragCoord 计算索引以获取输入A,然后循环所有其他行。通过将最近对的索引编码为颜色,通过 readPixels 读取结果。

  precision highp float;

  uniform sampler2D aValues;
  uniform vec2 aDimensions;  // the size of the aValues texture in pixels (texels)
  uniform sampler2D bValues;
  uniform vec2 bDimensions;  // the size of the bValues texture in pixels (texels)
  uniform sampler2D cValues;
  uniform vec2 cDimensions;  // the size of the cValues texture in pixels (texels)
  uniform vec2 outputDimensions; // the size of the thing we're drawing to (canvas)

  // this code, given a sampler2D, the size of the texture, and an index
  // computes a UV coordinate to pull one RGBA value out of a texture
  // as though the texture was a 1D array.
  vec3 getPoint(in sampler2D tex, in vec2 dimensions, in float index) {
    vec2 uv = (vec2(
       floor(mod(index, dimensions.x)),
       floor(index / dimensions.x)) + 0.5) / dimensions;
    return texture2D(tex, uv).xyz;
  }

  // from https://stackoverflow.com/a/6853926/128511
  float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) {
    vec3 ba = a - b;
    vec3 bc = c - b;
    float d = dot(ba, bc);
    float len = length(bc);
    float param = 0.0;
    if (len != 0.0) {
      param = clamp(d / (len * len), 0.0, 1.0);
    }
    vec3 r = b + bc * param;
    return distance(a, r);
  }

  void main() {
    // gl_FragCoord is the coordinate of the pixel that is being set by the fragment shader.
    // It is the center of the pixel so the bottom left corner pixel will be (0.5, 0.5).
    // the pixel to the left of that is (1.5, 0.5), The pixel above that is (0.5, 1.5), etc...
    // so we can compute back into a linear index 
    float ndx = floor(gl_FragCoord.y) * outputDimensions.x + floor(gl_FragCoord.x); 
    
    // find the closest points
    float minDist = 10000000.0; 
    float minIndex = -1.0;
    vec3 a = getPoint(aValues, aDimensions, ndx);
    for (int i = 0; i < ${bPoints.length / 4}; ++i) {
      vec3 b = getPoint(bValues, bDimensions, float(i));
      vec3 c = getPoint(cValues, cDimensions, float(i));
      float dist = distanceFromPointToLine(a, b, c);
      if (dist < minDist) {
        minDist = dist;
        minIndex = float(i);
      }
    }
    
    // convert to 8bit color. The canvas defaults to RGBA 8bits per channel
    // so take our integer index (minIndex) and convert to float values that
    // will end up as the same 32bit index when read via readPixels as
    // 32bit values.
    gl_FragColor = vec4(
      mod(minIndex, 256.0),
      mod(floor(minIndex / 256.0), 256.0),
      mod(floor(minIndex / (256.0 * 256.0)), 256.0) ,
      floor(minIndex / (256.0 * 256.0 * 256.0))) / 255.0;
  }

我只是猜测,一般来说,这可以通过某种空间结构更好地解决,这样你就不必检查每一行的每一点,但是像上面的代码这样的东西应该可以工作并且非常平行线。每个结果将由另一个 GPU 核心计算。

const v3 = twgl.v3;

// note: I'm using twgl to make the code smaller.
// This is not lesson in WebGL. You should already know what it means
// to setup buffers and attributes and set uniforms and create textures.
// What's important is the technique, not the minutia of WebGL. If you
// don't know how to do those things you need a much bigger tutorial
// on WebGL like https://webglfundamentals.org

function main() {
  const gl = document.createElement('canvas').getContext('webgl');
  const ext = gl.getExtension('OES_texture_float');
  if (!ext) {
    alert('need OES_texture_float');
    return;
  }
  
  const r = max => Math.random() * max;
  const hsl = (h, s, l) => `hsl(${h * 360},${s * 100 | 0}%,${l * 100 | 0}%)`;
  function createPoints(numPoints) {
    const points = [];
    for (let i = 0; i < numPoints; ++i) {
      points.push(r(300), r(150), 0, 0);  // RGBA
    }
    return points;
  }

  function distanceFromPointToLineSquared(a, b, c) {
    const ba = v3.subtract(a, b);
    const bc = v3.subtract(c, b);
    const dot = v3.dot(ba, bc);
    const lenSq = v3.lengthSq(bc);
    let param = 0;
    if (lenSq !== 0) {
      param = Math.min(1, Math.max(0, dot / lenSq));
    }
    const r = v3.add(b, v3.mulScalar(bc, param));
    return v3.distanceSq(a, r);
  }

  const aPoints = createPoints(6);
  const bPoints = createPoints(15);
  const cPoints = createPoints(15);
  
  // do it in JS to check
  {
    // compute closest lines to points
    const closest = [];
    for (let i = 0; i < aPoints.length; i += 4) {
      const a = aPoints.slice(i, i + 3);
      let minDistSq = Number.MAX_VALUE;
      let minIndex = -1;
      for (let j = 0; j < bPoints.length; j += 4) {
        const b = bPoints.slice(j, j + 3);
        const c = cPoints.slice(j, j + 3);
        const distSq = distanceFromPointToLineSquared(a, b, c);
        if (distSq < minDistSq) {
          minDistSq = distSq;
          minIndex = j / 4;
        }
      }
      closest.push(minIndex);
    }

    drawResults(document.querySelector('#js'), closest);
  }

  const vs = `
  attribute vec4 position;
  void main() {
    gl_Position = position;
  }
  `;
  
  const fs = `
  precision highp float;

  uniform sampler2D aValues;
  uniform vec2 aDimensions;  // the size of the aValues texture in pixels (texels)
  uniform sampler2D bValues;
  uniform vec2 bDimensions;  // the size of the bValues texture in pixels (texels)
  uniform sampler2D cValues;
  uniform vec2 cDimensions;  // the size of the cValues texture in pixels (texels)
  uniform vec2 outputDimensions; // the size of the thing we're drawing to (canvas)

  // this code, given a sampler2D, the size of the texture, and an index
  // computes a UV coordinate to pull one RGBA value out of a texture
  // as though the texture was a 1D array.
  vec3 getPoint(in sampler2D tex, in vec2 dimensions, in float index) {
    vec2 uv = (vec2(
       floor(mod(index, dimensions.x)),
       floor(index / dimensions.x)) + 0.5) / dimensions;
    return texture2D(tex, uv).xyz;
  }

  // from https://stackoverflow.com/a/6853926/128511
  float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) {
    vec3 ba = a - b;
    vec3 bc = c - b;
    float d = dot(ba, bc);
    float len = length(bc);
    float param = 0.0;
    if (len != 0.0) {
      param = clamp(d / (len * len), 0.0, 1.0);
    }
    vec3 r = b + bc * param;
    return distance(a, r);
  }

  void main() {
    // gl_FragCoord is the coordinate of the pixel that is being set by the fragment shader.
    // It is the center of the pixel so the bottom left corner pixel will be (0.5, 0.5).
    // the pixel to the left of that is (1.5, 0.5), The pixel above that is (0.5, 1.5), etc...
    // so we can compute back into a linear index 
    float ndx = floor(gl_FragCoord.y) * outputDimensions.x + floor(gl_FragCoord.x); 
    
    // find the closest points
    float minDist = 10000000.0; 
    float minIndex = -1.0;
    vec3 a = getPoint(aValues, aDimensions, ndx);
    for (int i = 0; i < ${bPoints.length / 4}; ++i) {
      vec3 b = getPoint(bValues, bDimensions, float(i));
      vec3 c = getPoint(cValues, cDimensions, float(i));
      float dist = distanceFromPointToLine(a, b, c);
      if (dist < minDist) {
        minDist = dist;
        minIndex = float(i);
      }
    }
    
    // convert to 8bit color. The canvas defaults to RGBA 8bits per channel
    // so take our integer index (minIndex) and convert to float values that
    // will end up as the same 32bit index when read via readPixels as
    // 32bit values.
    gl_FragColor = vec4(
      mod(minIndex, 256.0),
      mod(floor(minIndex / 256.0), 256.0),
      mod(floor(minIndex / (256.0 * 256.0)), 256.0) ,
      floor(minIndex / (256.0 * 256.0 * 256.0))) / 255.0;
  }
  `;
  
  // compile shader, link program, lookup locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  
  // calls gl.createBuffer, gl.bindBuffer, gl.bufferData for a -1 to +1 quad
  const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

  // make an RGBA float texture for each set of points
  // calls gl.createTexture, gl.bindTexture, gl.texImage2D, gl.texParameteri
  const aTex = twgl.createTexture(gl, {
    src: aPoints,
    width: aPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
  const bTex = twgl.createTexture(gl, {
    src: bPoints,
    width: bPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
  const cTex = twgl.createTexture(gl, {
    src: cPoints,
    width: cPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
    
  const numOutputs = aPoints.length / 4;
  gl.canvas.width = numOutputs;
  gl.canvas.height = 1;
  gl.viewport(0, 0, numOutputs, 1);
  
  gl.useProgram(programInfo.program);  
  
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  // calls gl.activeTexture, gl.bindTexture, gl.uniform
  twgl.setUniforms(programInfo, {
    aValues: aTex,
    aDimensions: [aPoints.length / 4, 1],
    bValues: cTex,
    bDimensions: [bPoints.length / 4, 1],
    cValues: bTex,
    cDimensions: [cPoints.length / 4, 1],
    outputDimensions: [aPoints.length / 4, 1],
  });
  
  // draw the quad
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  
  // get result
  const pixels = new Uint8Array(numOutputs * 4);
  const results = new Uint32Array(pixels.buffer);
  gl.readPixels(0, 0, numOutputs, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  drawResults(document.querySelector('#glsl'), results);


  function drawResults(canvas, closest) {
    const ctx = canvas.getContext('2d');
    
    // draw the lines
    ctx.beginPath();
    for (let j = 0; j < bPoints.length; j += 4) {
      const b = bPoints.slice(j, j + 2);
      const c = cPoints.slice(j, j + 2);
      ctx.moveTo(...b);
      ctx.lineTo(...c);
    }
    ctx.strokeStyle = '#888';
    ctx.stroke();
    
    // draw the points and closest lines
    for (let i = 0; i < aPoints.length; i += 4) {
      const a = aPoints.slice(i, i + 2);
      const ndx = closest[i / 4] * 4;
      const b = bPoints.slice(ndx, ndx + 2);
      const c = cPoints.slice(ndx, ndx + 2);
      const color = hsl(i / aPoints.length, 1, 0.4);
      ctx.fillStyle = color;
      ctx.strokeStyle = color;
      ctx.fillRect(a[0] - 2, a[1] - 2, 5, 5);
      ctx.beginPath();
      ctx.moveTo(...b);
      ctx.lineTo(...c);
      ctx.stroke();
    }
  }

}
main();
canvas { border: 1px solid black; margin: 5px; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<div>glsl</div>
<canvas id="glsl"></canvas>
<div>js</div>
<canvas id="js"></canvas>

如果你使用 WebGL2 那么你可以使用texelFetch 所以getPoint 变成了

vec3 getPoint(in sampler2D tex, in int index) {
  ivec2 size = textureSize(tex, 0);
  ivec2 uv = ivec2(index % size.x, index / size.x);
  return texelFetch(tex, uv, 0).xyz;
}

您不需要传递输入纹理的大小,只需传递输出大小。您也可以将输出设为 R32U 并输出无符号整数索引,因此无需对结果进行编码。

注意:代码假定您为每个 a、b 和 c 执行的值少于 2048 个,因此大部分代码假定一维纹理。如果您需要超过 2048 个值,则需要调整代码以制作适合您数据的大小的矩形纹理,例如,如果您有 9000 个值,那么 9x1000 纹理将起作用。如果你有 8999 个值,那么你仍然需要一个 9x1000 的纹理来填充一个矩形,因为纹理是二维数组。

另请注意,调用 readPixels 被认为很慢。例如,如果您只想像上面那样绘制结果,而不是渲染到画布并通过 readPixels 读取值,您可以将结果渲染到纹理,然后将纹理传递给另一个着色器。


附录

这可能是错误的地方,但作为对此类内容的 GLSL 的简洁解释,您可以将 GLSL 视为 Array.prototype.map 的精美版本。当您使用map 时,您不会选择直接写入的内容。它是间接发生的。

const a = [1, 2, 3, 4, 5];
const b = a.map((v, index) => { return v * 2 + index; });

{ return v * 2 + index} 部分类似于着色器。在 JavaScript 中,map 中的函数返回值。在 GLSL ES 1.0 中,着色器将 gl_FragColor 设置为输出。在 Javascript 中,index 是要写入的数组的索引(并且恰好也是输入数组的索引)。在 GLSL 中,gl_FragCoord 扮演同样的角色。

否则,顶点着色器的输出决定了将写入哪些像素(二维数组的哪些数组元素),从而使其成为map 的更具选择性的版本。在上面的代码中,我们绘制了一个 -1 到 +1 的四边形,有效地表示“映射所有像素”。

实际上这里是上面代码的一个版本,没有 GLSL,只有 JavaScript,但是 JavaScript 重新构造后看起来更像 GLSL。

const v3 = twgl.v3;

function main() {
  
  const r = max => Math.random() * max;
  const hsl = (h, s, l) => `hsl(${h * 360},${s * 100 | 0}%,${l * 100 | 0}%)`;

  function createPoints(numPoints) {
    const points = [];
    for (let i = 0; i < numPoints; ++i) {
      points.push(r(300), r(150), 0, 0);  // RGBA
    }
    return points;
  }

  function distanceFromPointToLineSquared(a, b, c) {
    const ba = v3.subtract(a, b);
    const bc = v3.subtract(c, b);
    const dot = v3.dot(ba, bc);
    const lenSq = v3.lengthSq(bc);
    let param = 0;
    if (lenSq !== 0) {
      param = Math.min(1, Math.max(0, dot / lenSq));
    }
    const r = v3.add(b, v3.mulScalar(bc, param));
    return v3.distanceSq(a, r);
  }

  const aPoints = createPoints(6);
  const bPoints = createPoints(15);
  const cPoints = createPoints(15);
  
  const gl_FragCoord = {};
  let gl_FragColor;
  
  const aValues = aPoints;
  const aDimensions = {}; // N/A
  const bValues = bPoints;
  const bDimensions = {}; // N/A
  const cValues = cPoints;
  const cDimensions = {}; // N/A
  const outputDimensions = {x: aPoints.length / 4, y: 1 };
  
  function getPoint(sampler, dimension, ndx) {
    return sampler.slice(ndx * 4, ndx * 4 + 3);
  }
  
  function javaScriptFragmentShader() {
    // gl_FragCoord is the coordinate of the pixel that is being set by the fragment shader.
    // It is the center of the pixel so the bottom left corner pixel will be (0.5, 0.5).
    // the pixel to the left of that is (1.5, 0.5), The pixel above that is (0.5, 1.5), etc...
    // so we can compute back into a linear index 
    const ndx = Math.floor(gl_FragCoord.y) * outputDimensions.x + Math.floor(gl_FragCoord.x); 
    
    // find the closest points
    let minDist = 10000000.0; 
    let minIndex = -1.0;
    const a = getPoint(aValues, aDimensions, ndx);
    for (let i = 0; i < bPoints.length / 4; ++i) {
      const b = getPoint(bValues, bDimensions, i);
      const c = getPoint(cValues, cDimensions, i);
      const dist = distanceFromPointToLineSquared(a, b, c);
      if (dist < minDist) {
        minDist = dist;
        minIndex = i;
      }
    }
    
    // convert to 8bit color. The canvas defaults to RGBA 8bits per channel
    // so take our integer index (minIndex) and convert to float values that
    // will end up as the same 32bit index when read via readPixels as
    // 32bit values.
    gl_FragColor = [
      minIndex % 256.0,
      Math.floor(minIndex / 256.0) % 256.0,
      Math.floor(minIndex / (256.0 * 256.0)) % 256.0,
      Math.floor(minIndex / (256.0 * 256.0 * 256.0)),
    ].map(v => v / 255.0);
  }
  
  // do it in JS to check
  {
    // compute closest lines to points
    
    const closest = [];
    const width = aPoints.length / 4;
    const height = 1;
    
    // WebGL drawing each pixel
    for (let y = 0; y < height; ++y) {
      for (let x = 0; x < width; ++x) {
        gl_FragCoord.x = x + 0.5;  // because pixels represent a rectangle one unit wide in pixel space
        gl_FragCoord.y = y + 0.5;  // so the center of each pixel in the middle of that rectangle
        javaScriptFragmentShader();
        const index = gl_FragColor[0] * 255 +
                      gl_FragColor[1] * 255 * 256 +
                      gl_FragColor[2] * 255 * 256 * 256 +
                      gl_FragColor[3] * 255 * 256 * 256 * 256;
        closest.push(index);
      }
    }

    drawResults(document.querySelector('#js'), closest);
  }

  function drawResults(canvas, closest) {
    const ctx = canvas.getContext('2d');
    
    // draw the lines
    ctx.beginPath();
    for (let j = 0; j < bPoints.length; j += 4) {
      const b = bPoints.slice(j, j + 2);
      const c = cPoints.slice(j, j + 2);
      ctx.moveTo(...b);
      ctx.lineTo(...c);
    }
    ctx.strokeStyle = '#888';
    ctx.stroke();
    
    // draw the points and closest lines
    for (let i = 0; i < aPoints.length; i += 4) {
      const a = aPoints.slice(i, i + 2);
      const ndx = closest[i / 4] * 4;
      const b = bPoints.slice(ndx, ndx + 2);
      const c = cPoints.slice(ndx, ndx + 2);
      const color = hsl(i / aPoints.length, 1, 0.4);
      ctx.fillStyle = color;
      ctx.strokeStyle = color;
      ctx.fillRect(a[0] - 2, a[1] - 2, 5, 5);
      ctx.beginPath();
      ctx.moveTo(...b);
      ctx.lineTo(...c);
      ctx.stroke();
    }
  }

}
main();
canvas { border: 1px solid black; margin: 5px; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas id="js"></canvas>

【讨论】:

  • 我怀疑你正确地理解了我的问题,我希望我能对你的回答说同样的话,这远远超出了我目前对 GLSL 的理解水平。我会在接下来的几天里学习并接受它 - 非常感谢!
  • 您对哪一部分理解有困难?添加了一些cmets。还有here's some WebGL tutorials
  • 这不是你,是我。我得到了一般的概念和想法,但我还不熟悉 GLSL,所以现在有些概念仍然模糊不清。不幸的是,我最不清楚的部分是读/写数据。我会尽快浏览教程。我已经很感激这个答案和你过去给我的其他答案
  • 是的,我了解到您对 GLSL 不太熟悉。我添加了一些更多的东西以防万一。我知道这不像几段解释那么简单,但也许会有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-11
  • 1970-01-01
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 2013-01-26
相关资源
最近更新 更多