Poisson-Disc 算法介绍

944 阅读1分钟

这是我参与8月更文挑战的第11天,活动详情查看: 8月更文挑战

动画链接:bl.ocks.org/mbostock/db…  

Poisson-Disc 算法介绍

  1. 红点代表 “活跃” 的样本。在每次迭代中,从该组的所有活跃样本中随机地选择一个样本。接着在样本周围的环形区域内随机产生新的候选样本(如动画中黑圈白底的点),最多产生 k 次。

  2. 环带的半径范围为 (r, 2r),其中 r 是任意两个样本之间的最小允许距离。如果产生的新候选样本落在了现有样本半径为 r 的范围内(即动画中的灰色禁区)将被拒绝,与现有的样本相连的黑色线表示新候选点太靠近现有样本点。如果候选样本点满足以上条件并被接受(即和周围的现有样本点距离大于等于 r ),它将被作为新的活跃样本(红色)。

  3. 如果第 k 个候选样本点仍然是是不可接受的,那么所选择的“活跃”样本点将被标记为无效(非活性),将不再用于产生候选样本点。非活性的样本点用黑色表示。

  4. 当没有样本保持活跃的时候,算法则结束。

【尺寸为 R2\frac{R}{\sqrt{2}} 的背景网格是用来加速每个候选的距离检查。因为每个小格只能包含至多一个样本,所以只需要检查固定数目的相邻小格即可】

​ 具体实现代码如下:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.grid {
  stroke: #000;
  stroke-opacity: .15;
  shape-rendering: crispEdges;
}

.exclusion {
  fill: #ccc;
}

.candidate-connection,
.candidate {
  fill: #fff;
  stroke: #000;
  stroke-width: 1.5px;
}

.candidate-annulus {
  fill: #000;
  fill-opacity: .25;
  stroke: #000;
  stroke-width: 1.5px;
}

.sample--active {
  fill: #f00;
  stroke: #f00;
  stroke-width: 2px;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>

var width = 960,
    height = 500;

var k = 30, // maximum number of samples before rejection
    radius = 50,
    radius2 = radius * radius,
    R = 3 * radius2,
    cellSize = radius * Math.SQRT1_2,
    gridWidth = Math.ceil(width / cellSize),
    gridHeight = Math.ceil(height / cellSize),
    grid = new Array(gridWidth * gridHeight),
    queue = [],
    queueSize = 0;

var arcEmptyAnnulus = d3.svg.arc()
    .innerRadius(radius)
    .outerRadius(radius)
    .startAngle(0)
    .endAngle(2 * Math.PI)();

var arcAnnulus = d3.svg.arc()
    .innerRadius(radius)
    .outerRadius(radius * 2)
    .startAngle(0)
    .endAngle(2 * Math.PI)();

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

var gExclusion = svg.append("g")
    .attr("class", "exclusion");

svg.append("path")
    .attr("class", "grid")
    .attr("d", d3.range(cellSize, width, cellSize)
        .map(function(x) { return "M" + Math.round(x) + ",0V" + height; })
        .join("")
      + d3.range(cellSize, height, cellSize)
        .map(function(y) { return "M0," + Math.round(y) + "H" + width; })
        .join(""));

var searchAnnulus = svg.append("path")
    .attr("class", "candidate-annulus");

var gConnection = svg.append("g")
    .attr("class", "candidate-connection");

var gSample = svg.append("g")
    .attr("class", "sample");

var gCandidate = svg.append("g")
    .attr("class", "candidate");

sample(Math.random() * width, Math.random() * height);

setTimeout(function selectActive() {
  var i = Math.random() * queueSize | 0,
      s = queue[i],
      j = 0;

  gCandidate
      .style("opacity", null);

  gConnection
      .style("opacity", null);

  searchAnnulus
      .style("opacity", null)
      .style("stroke-opacity", 0)
      .attr("transform", "translate(" + s + ")")
      .attr("d", arcEmptyAnnulus)
    .transition()
      .attr("d", arcAnnulus)
      .style("stroke-opacity", 1)
      .each("end", generateCandidate);

  var sampleActive = gSample.selectAll("circle")
    .filter(function(d) { return d === s; });

  function generateCandidate() {
    if (++j > k) return rejectActive();

    var a = 2 * Math.PI * Math.random(),
        r = Math.sqrt(Math.random() * R + radius2),
        x = s[0] + r * Math.cos(a),
        y = s[1] + r * Math.sin(a);

    // Reject candidates that are outside the allowed extent.
    if (0 > x || x >= width || 0 > y || y >= height) return generateCandidate();

    // If this is an acceptable candidate, create a new sample;
    // otherwise, generate a new candidate.
    gCandidate.append("circle")
        .attr("r", 1e-6)
        .attr("cx", x)
        .attr("cy", y)
      .transition()
        .attr("r", 3.75)
        .each("end", far(x, y) ? acceptCandidate : generateCandidate);

    function acceptCandidate() {
      removeCandidates()
          .each("end", queueSize ? selectActive : null);

      sample(x, y);
    }
  }

  function rejectActive() {
    queue[i] = queue[--queueSize];
    queue.length = queueSize;

    removeCandidates()
        .each("end", queueSize ? selectActive : null);

    sampleActive
        .classed("sample--active", false);
  }

  function removeCandidates() {
    gCandidate.transition()
        .style("opacity", 0)
      .selectAll("circle")
        .remove();

    gConnection.transition()
        .style("opacity", 0)
      .selectAll("line")
        .remove();

    return searchAnnulus.transition()
        .style("opacity", 0);
  }
}, 250);

function far(x, y) {
  var i = x / cellSize | 0,
      j = y / cellSize | 0,
      i0 = Math.max(i - 2, 0),
      j0 = Math.max(j - 2, 0),
      i1 = Math.min(i + 3, gridWidth),
      j1 = Math.min(j + 3, gridHeight);

  for (j = j0; j < j1; ++j) {
    var o = j * gridWidth;
    for (i = i0; i < i1; ++i) {
      if (s = grid[o + i]) {
        var s,
            dx = s[0] - x,
            dy = s[1] - y;
        if (dx * dx + dy * dy < radius2) {
          gConnection.append("line")
              .attr("x1", x)
              .attr("y1", y)
              .attr("x2", x)
              .attr("y2", y)
            .transition()
              .attr("x2", s[0])
              .attr("y2", s[1]);

          return false;
        }
      }
    }
  }

  return true;
}

function sample(x, y) {
  var s = [x, y];

  gExclusion.append("circle")
      .attr("r", 1e-6)
      .attr("cx", x)
      .attr("cy", y)
    .transition()
      .attr("r", radius);

  gSample.append("circle")
      .datum(s)
      .attr("class", "sample--active")
      .attr("r", 1e-6)
      .attr("cx", x)
      .attr("cy", y)
    .transition()
      .attr("r", 3);

  queue.push(s);
  grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s;
  ++queueSize;
  return s;
}

</script>