절차 적으로 생성 된 세계 덩어리를 다른 세계 덩어리와 일치


18

Roger Zelazny의 앰버 연대기를 읽었습니까?

3 인칭 MMO 게임을한다고 상상해보십시오. 당신은 세계에서 산란하고 돌아 다니기 시작합니다. 얼마 후, 당신이 생각할 때, 당신은지도를 배웠고, 당신은 한 번도 본 적이없고, 당신이 한 번도 본 적이 없다는 것을 깨닫습니다. 당신은 당신이 알고 있다고 확신하는 마지막 장소로 돌아가서 여전히 거기 있습니다. 그러나 세계의 다른 곳도 바뀌었고 어떻게되었는지조차 알지 못했습니다.

절차 적 세계 생성에 대해 읽었습니다. 나는 Perlin 노이즈와 옥타브, Simplex 노이즈, Diamond-square 알고리즘, 지각판 시뮬레이션 및 물 침식에 대해 읽었습니다. 나는 절차 적 세계 세대의 일반적인 접근 방식에 대해 약간의 이해가 있다고 생각합니다.

그리고이 지식으로 나는 당신이 위에서 쓴 것과 같은 것을 어떻게 할 수 있는지 전혀 모른다. 내 마음에 오는 모든 아이디어에는 몇 가지 이론적 인 문제가 있습니다. 내가 생각할 수있는 몇 가지 아이디어는 다음과 같습니다.

1) 시드 번호를 입력으로 사용하고 일부는 완전히 설명 할 수있는 "가역"세계 세대

나는 그것이 가능하다는 것을 의심하지만, 씨앗을 받고 덩어리가 생성되는 숫자 행렬을 생성하는 함수를 상상합니다. 그리고 각 고유 번호에는 고유 한 청크가 있습니다. 두 번째 함수는이 고유 한 청크 번호를 가져와이 번호가 포함 된 시드를 생성합니다. 아래 그림에서 구성표를 만들려고했습니다.

여기에 이미지 설명을 입력하십시오

2) 청크를 완전히 무작위로 만들고 그 사이에서 전환합니다.

Aracthor은 제안했다. 이 방법의 장점은 가능하고 마술 기능이 필요하지 않다는 것입니다. :)

이 접근 방식의 단점은 다양한 세상을 가질 수 없다는 것입니다. 군도와 대륙이 하나의 숫자로 표시되고 인접 청크라고 가정하면 청크의 크기가 대륙과 같지 않을 것입니다. 그리고 청크간에 잘 보이는 전환이 가능하다는 것을 의심합니다. 뭔가 빠졌습니까?

다시 말해, 절차 적으로 생성 된 세계로 MMO를 개발하고 있습니다. 그러나 하나의 세계를 갖는 대신 많은 것이 있습니다. 월드를 생성하기 위해 어떤 접근 방식을 취하고 플레이어가 전환을 인식하지 않고 한 월드에서 다른 월드로 플레이어의 전환을 어떻게 구현할 것입니까?

어쨌든, 나는 당신이 일반적인 생각을 가지고 있다고 생각합니다. 어떻게 했어?


그래서 나는 여기에 대한 답변에 문제가 있습니다. @Aracthor 나는 전에 부드러운 매니 폴드에 대해 이야기했지만, 여기에 적용됩니다. 그러나 2 개의 매우 높은 답변이 있으므로 요점이 있는지 궁금합니다.
Alec Teal

추가해야 할 것이 있으면 @AlecTeal하시기 바랍니다. 나는 어떤 아이디어 나 제안을 듣고 기쁘다.
netaholic

답변:


23

고차 노이즈 슬라이스를 사용하십시오. 이전에 높이 맵에 2d 노이즈를 사용한 경우 대신 마지막 좌표가 고정 된 3D 노이즈를 사용하십시오. 이제 마지막 치수에서 위치를 천천히 변경하여 지형을 수정할 수 있습니다. Perlin 노이즈는 모든 차원에서 연속적이기 때문에 노이즈 함수를 샘플링하는 위치를 부드럽게 변경하는 한 부드럽게 전환 할 수 있습니다.

예를 들어, 거리를 플레이어에서 멀리 떨어진 지형 만 오프셋으로 변경하려는 경우. 각 좌표에 대한 오프셋을 맵에 저장하고 늘리거나 줄일 수는 없습니다. 이런 식으로지도는 더 새롭지 만 더 오래되지는 않습니다.

이 아이디어는 이미 3D 노이즈를 사용하고 있다면 4D에서 샘플링 한 후에도 작동합니다. 또한 Simplex 노이즈를 살펴보십시오. Perlin 노이즈의 개선 된 버전이며 더 많은 치수에서 더 잘 작동합니다.


2
이것은 흥미 롭다. 3d 노이즈를 생성하고 특정 z에서 xy- 슬라이스를 하이트 맵으로 사용하고 플레이어와의 거리가 증가함에 따라 z 좌표를 변경하여 다른 슬라이스로 부드럽게 전환하는 것이 좋습니다.
netaholic

@netaholic 맞습니다. 슬라이스로 설명하는 것은 매우 좋은 직관입니다. 또한지도의 모든 위치에서 마지막 좌표에 대한 최고 값을 추적 할 수 있으며 증가시킬뿐 절대 감소하지는 않습니다.
danijar

1
이것은 훌륭한 아이디어입니다. 기본적으로 지형지도는 3D 볼륨을 통한 포물선 (또는 다른 곡선) 슬라이스입니다.
가짜 이름

이것은 정말 영리한 아이디어입니다.
user253751

5

세상을 여러 덩어리로 나누는 아이디어는 나쁘지 않습니다. 불완전하다.

유일한 문제는 청크 간의 접합입니다. 예를 들어, 펄린 노이즈를 사용하여 릴리프를 생성하고 각 청크마다 다른 시드를 생성하면 이러한 상황이 발생할 위험이 있습니다.

청크 릴리프 버그

해결책은 Perlin 노이즈 시드뿐만 아니라 주변의 다른 청크에서도 청크 릴리프를 생성하는 것입니다.

Perlin 알고리즘은 주변의 임의 맵 값을 사용하여 "부드럽게"만듭니다. 공통 맵을 사용하면 함께 매끄럽게됩니다.

유일한 문제는 플레이어가 물러날 때 덩어리 시드를 바꾸면 덩어리가 다시로드되어야한다는 것입니다. 경계도 변경되어야하기 때문입니다.

청크의 크기는 변경되지 않지만, 청크가 볼 때 청크를로드해야하고이 방법을 사용하면 인접 청크도 너무 커야하기 때문에 플레이어에서로드 / 언로드되는 최소 거리가 증가합니다. .

최신 정보:

세계의 각 덩어리가 다른 유형이면 문제가 커집니다. 이것은 단지 구호에 관한 것이 아닙니다. 비용이 많이 드는 솔루션은 다음과 같습니다.

청크 컷

녹색 덩어리가 삼림 세계, 파란색 것 열도 및 노란색 것은 평평한 사막이라고 가정 해 봅시다.
여기서 해결책은 구호 및 지상 특성 (접지 된 물체 또는 원하는 다른 물체)이 한 유형에서 다른 유형으로 점차 바뀌는 "전환"영역을 만드는 것입니다.

그리고이 그림에서 볼 수 있듯이, 코드를 작성하는 데 필요한 부분은 청크 모서리의 작은 사각형 일 것입니다. 그것들은 잠재적으로 다른 성질을 가진 4 개의 청크 사이를 연결해야합니다.

따라서이 복잡성 수준에서는 Perlin2D와 같은 클래식 2D 세계 세대를 사용할 수 없다고 생각합니다. @danijar 답변을 참조하십시오.


시드에서 청크의 "중심"을 생성하고 인접 청크를 기준으로 가장자리를 "부드럽게"제안합니까? 그것은 의미가 있지만, 그것은 청크의 크기를 증가시킬 것입니다. 그것은 영역의 크기 여야하기 때문에, 플레이어는 인접한 청크에 대한 천이 영역의 폭을 더한 두 배로 관찰 할 수 있습니다. 그리고 청크 영역은 세계가 다양할수록 더 커집니다.
netaholic

@netaholic 더 크지는 않지만 일종의 것입니다. 나는 그것에 단락을 추가했습니다.
Aracthor

내 질문을 업데이트했습니다. 내가 가진 몇 가지 아이디어를 설명하려고 노력했다
netaholic

따라서 다른 답변은 차트로 3 차원을 사용합니다 (정확하지는 않지만). 또한 평면을 매니 폴드로보고, 나는 당신의 아이디어를 좋아합니다. 조금 더 확장하려면 부드러운 매니 폴드가 필요합니다. 전환이 매끄 럽도록해야합니다. 그런 다음 흐리게 처리하거나 노이즈를 적용하면 대답이 완벽합니다.
Alec Teal

0

danijar의 아이디어는 매우 견고하지만 로컬 영역을 동일하게 유지하고 거리를 이동하려는 경우 많은 데이터를 저장할 수 있습니다. 점점 더 복잡한 소음을 점점 더 많이 요청합니다. 이 모든 것을보다 표준적인 2D 방식으로 얻을 수 있습니다.

나는 무한하고 결정적으로 고정 된 다이아몬드 사각형 알고리즘기반으로 무작위 프랙탈 노이즈를 절차 적으로 생성 하는 알고리즘 을 개발 했습니다 . 그래서 다이아몬드 스퀘어는 무한한 풍경뿐만 아니라 내 자신의 상당히 블록 알고리즘을 만들 수 있습니다.

아이디어는 기본적으로 동일합니다. 그러나 더 높은 차원 노이즈를 샘플링하는 대신 다른 반복 레벨에서 값을 반복 할 수 있습니다.

따라서 이전에 요청한 값을 저장하고 캐시합니다 (이 체계는 독립적으로 이미 초고속 알고리즘의 속도를 높이는 데 사용될 수 있음). 그리고 새로운 영역이 요청되면 새로운 y 값으로 생성됩니다. 해당 요청에서 요청되지 않은 영역은 제거됩니다.

따라서 추가 차원에서 다른 공간을 좁히는 대신. 우리는 서로 다른 (다른 레벨에서 점차적으로 많은 양으로) 혼합하기 위해 여분의 비트 데이터를 저장합니다.

사용자가 한 방향으로 이동하면 그에 따라 값이 이동하고 각 레벨에서 새 값이 생성됩니다. 최상위 반복 시드가 변경되면 전 세계가 크게 바뀌게됩니다. 최종 반복에 다른 결과가 제공되면 변경 량은 +1 ​​블록 정도가됩니다. 그러나 언덕은 여전히 ​​거기에 있고 계곡 등은있을 것이나 구석과 채석장은 바뀌었을 것입니다. 멀리 가지 않으면 언덕이 사라질 것입니다.

따라서 각 반복마다 100x100 청크 값을 저장하면. 그런 다음 플레이어에서 100x100으로 변경할 수 없습니다. 그러나 200x200에서 상황은 1 블록 씩 바뀔 수 있습니다. 400x400에서는 상황이 2 블록 씩 변경 될 수 있습니다. 800x800 거리에서 사물은 4 블록 씩 변경 될 수 있습니다. 따라서 상황이 바뀌고 갈수록 더 많이 바뀔 것입니다. 당신이 돌아 가면 그들은 다를 것입니다, 당신이 너무 멀리 가면 모든 씨앗이 버려 질 때 완전히 바뀌고 완전히 잃어 버릴 것입니다.

이 안정화 효과를 제공하기 위해 다른 차원을 추가하면 확실히 작동하여 y를 멀리 이동시킬 수 있지만 필요하지 않을 때 많은 블록에 대해 많은 데이터를 저장합니다. 결정적 프랙탈 노이즈 알고리즘에서는 위치가 특정 지점을 넘어서 이동할 때 값을 변경 (다른 양으로)하여 동일한 효과를 얻을 수 있습니다.

https://jsfiddle.net/rkdzau7o/

var SCALE_FACTOR = 2;
//The scale factor is kind of arbitrary, but the code is only consistent for 2 currently. Gives noise for other scale but not location proper.
var BLUR_EDGE = 2; //extra pixels are needed for the blur (3 - 1).
var buildbuffer = BLUR_EDGE + SCALE_FACTOR;

canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
var stride = canvas.width + buildbuffer;
var colorvalues = new Array(stride * (canvas.height + buildbuffer));
var iterations = 7;
var xpos = 0;
var ypos = 0;
var singlecolor = true;


/**
 * Function adds all the required ints into the ints array.
 * Note that the scanline should not actually equal the width.
 * It should be larger as per the getRequiredDim function.
 *
 * @param iterations Number of iterations to perform.
 * @param ints       pixel array to be used to insert values. (Pass by reference)
 * @param stride     distance in the array to the next requestedY value.
 * @param x          requested X location.
 * @param y          requested Y location.
 * @param width      width of the image.
 * @param height     height of the image.
 */

function fieldOlsenNoise(iterations, ints, stride, x, y, width, height) {
  olsennoise(ints, stride, x, y, width, height, iterations); //Calls the main routine.
  //applyMask(ints, stride, width, height, 0xFF000000);
}

function applyMask(pixels, stride, width, height, mask) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) {
    for (var j = 0, m = width - 1; j <= m; j++) {
      pixels[index + j] |= mask;
    }
  }
}

/**
 * Converts a dimension into the dimension required by the algorithm.
 * Due to the blurring, to get valid data the array must be slightly larger.
 * Due to the interpixel location at lowest levels it needs to be bigger by
 * the max value that can be. (SCALE_FACTOR)
 *
 * @param dim
 * @return
 */

function getRequiredDim(dim) {
  return dim + BLUR_EDGE + SCALE_FACTOR;
}

//Function inserts the values into the given ints array (pass by reference)
//The results will be within 0-255 assuming the requested iterations are 7.
function olsennoise(ints, stride, x_within_field, y_within_field, width, height, iteration) {
  if (iteration == 0) {
    //Base case. If we are at the bottom. Do not run the rest of the function. Return random values.
    clearValues(ints, stride, width, height); //base case needs zero, apply Noise will not eat garbage.
    applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
    return;
  }

  var x_remainder = x_within_field & 1; //Adjust the x_remainder so we know how much more into the pixel are.
  var y_remainder = y_within_field & 1; //Math.abs(y_within_field % SCALE_FACTOR) - Would be assumed for larger scalefactors.

  /*
  Pass the ints, and the stride for that set of ints.
  Recurse the call to the function moving the x_within_field forward if we actaully want half a pixel at the start.
  Same for the requestedY.
  The width should expanded by the x_remainder, and then half the size, with enough extra to store the extra ints from the blur.
  If the width is too long, it'll just run more stuff than it needs to.
  */

  olsennoise(ints, stride,
    (Math.floor((x_within_field + x_remainder) / SCALE_FACTOR)) - x_remainder,
    (Math.floor((y_within_field + y_remainder) / SCALE_FACTOR)) - y_remainder,
    (Math.floor((width + x_remainder) / SCALE_FACTOR)) + BLUR_EDGE,
    (Math.floor((height + y_remainder) / SCALE_FACTOR)) + BLUR_EDGE, iteration - 1);

  //This will scale the image from half the width and half the height. bounds.
  //The scale function assumes you have at least width/2 and height/2 good ints.
  //We requested those from olsennoise above, so we should have that.

  applyScaleShift(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE, SCALE_FACTOR, x_remainder, y_remainder);

  //This applies the blur and uses the given bounds.
  //Since the blur loses two at the edge, this will result
  //in us having width requestedX height of good ints and required
  // width + blurEdge of good ints. height + blurEdge of good ints.
  applyBlur(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE);

  //Applies noise to all the given ints. Does not require more or less than ints. Just offsets them all randomly.
  applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
}



function applyNoise(pixels, stride, x_within_field, y_within_field, width, height, iteration) {
  var bitmask = 0b00000001000000010000000100000001 << (7 - iteration);
  var index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY positions. Offsetting the index by stride each time.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX positions through width.
      var current = index + j; // The current position of the pixel is the index which will have added stride each, requestedY iteration
      pixels[current] += hashrandom(j + x_within_field, k + y_within_field, iteration) & bitmask;
      //add on to this pixel the hash function with the set reduction.
      //It simply must scale down with the larger number of iterations.
    }
  }
}

function applyScaleShift(pixels, stride, width, height, factor, shiftX, shiftY) {
  var index = (height - 1) * stride; //We must iteration backwards to scale so index starts at last Y position.
  for (var k = 0, n = height - 1; k <= n; n--, index -= stride) { // we iterate the requestedY, removing stride from index.
    for (var j = 0, m = width - 1; j <= m; m--) { // iterate the requestedX positions from width to 0.
      var pos = index + m; //current position is the index (position of that scanline of Y) plus our current iteration in scale.
      var lower = (Math.floor((n + shiftY) / factor) * stride) + Math.floor((m + shiftX) / factor); //We find the position that is half that size. From where we scale them out.
      pixels[pos] = pixels[lower]; // Set the outer position to the inner position. Applying the scale.
    }
  }
}

function clearValues(pixels, stride, width, height) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY values.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX values.
      pixels[index + j] = 0; //clears those values.
    }
  }
}

//Applies the blur.
//loopunrolled box blur 3x3 in each color.
function applyBlur(pixels, stride, width, height) {
  var index = 0;
  var v0;
  var v1;
  var v2;

  var r;
  var g;
  var b;

  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;

      v0 = pixels[pos];
      v1 = pixels[pos + 1];
      v2 = pixels[pos + 2];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
  index = 0;
  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;
      v0 = pixels[pos];
      v1 = pixels[pos + stride];
      v2 = pixels[pos + (stride << 1)];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
}


function hashrandom(v0, v1, v2) {
  var hash = 0;
  hash ^= v0;
  hash = hashsingle(hash);
  hash ^= v1;
  hash = hashsingle(hash);
  hash ^= v2;
  hash = hashsingle(hash);
  return hash;
}

function hashsingle(v) {
  var hash = v;
  var h = hash;

  switch (hash & 3) {
    case 3:
      hash += h;
      hash ^= hash << 32;
      hash ^= h << 36;
      hash += hash >> 22;
      break;
    case 2:
      hash += h;
      hash ^= hash << 22;
      hash += hash >> 34;
      break;
    case 1:
      hash += h;
      hash ^= hash << 20;
      hash += hash >> 2;
  }
  hash ^= hash << 6;
  hash += hash >> 10;
  hash ^= hash << 8;
  hash += hash >> 34;
  hash ^= hash << 50;
  hash += hash >> 12;
  return hash;
}


//END, OLSEN NOSE.



//Nuts and bolts code.

function MoveMap(dx, dy) {
  xpos -= dx;
  ypos -= dy;
  drawMap();
}

function drawMap() {
  //int iterations, int[] ints, int stride, int x, int y, int width, int height
  console.log("Here.");
  fieldOlsenNoise(iterations, colorvalues, stride, xpos, ypos, canvas.width, canvas.height);
  var img = ctx.createImageData(canvas.width, canvas.height);

  for (var y = 0, h = canvas.height; y < h; y++) {
    for (var x = 0, w = canvas.width; x < w; x++) {
      var standardShade = colorvalues[(y * stride) + x];
      var pData = ((y * w) + x) * 4;
      if (singlecolor) {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = standardShade & 0xFF;
        img.data[pData + 2] = standardShade & 0xFF;
      } else {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = (standardShade >> 8) & 0xFF;
        img.data[pData + 2] = (standardShade >> 16) & 0xFF;
      }
      img.data[pData + 3] = 255;
    }
  }
  ctx.putImageData(img, 0, 0);
}

$("#update").click(function(e) {
  iterations = parseInt($("iterations").val());
  drawMap();
})
$("#colors").click(function(e) {
  singlecolor = !singlecolor;
  drawMap();
})

var m = this;
m.map = document.getElementById("canvas");
m.width = canvas.width;
m.height = canvas.height;

m.hoverCursor = "auto";
m.dragCursor = "url(data:image/vnd.microsoft.icon;base64,AAACAAEAICACAAcABQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA/AAAAfwAAAP+AAAH/gAAB/8AAAH/AAAB/wAAA/0AAANsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////////////////////////////////////////////////////////////////////////////////gH///4B///8Af//+AD///AA///wAH//+AB///wAf//4AH//+AD///yT/////////////////////////////8=), default";
m.scrollTime = 300;

m.mousePosition = new Coordinate;
m.mouseLocations = [];
m.velocity = new Coordinate;
m.mouseDown = false;
m.timerId = -1;
m.timerCount = 0;

m.viewingBox = document.createElement("div");
m.viewingBox.style.cursor = m.hoverCursor;

m.map.parentNode.replaceChild(m.viewingBox, m.map);
m.viewingBox.appendChild(m.map);
m.viewingBox.style.overflow = "hidden";
m.viewingBox.style.width = m.width + "px";
m.viewingBox.style.height = m.height + "px";
m.viewingBox.style.position = "relative";
m.map.style.position = "absolute";

function AddListener(element, event, f) {
  if (element.attachEvent) {
    element["e" + event + f] = f;
    element[event + f] = function() {
      element["e" + event + f](window.event);
    };
    element.attachEvent("on" + event, element[event + f]);
  } else
    element.addEventListener(event, f, false);
}

function Coordinate(startX, startY) {
  this.x = startX;
  this.y = startY;
}

var MouseMove = function(b) {
  var e = b.clientX - m.mousePosition.x;
  var d = b.clientY - m.mousePosition.y;
  MoveMap(e, d);
  m.mousePosition.x = b.clientX;
  m.mousePosition.y = b.clientY;
};

/**
 * mousedown event handler
 */
AddListener(m.viewingBox, "mousedown", function(e) {
  m.viewingBox.style.cursor = m.dragCursor;

  // Save the current mouse position so we can later find how far the
  // mouse has moved in order to scroll that distance
  m.mousePosition.x = e.clientX;
  m.mousePosition.y = e.clientY;

  // Start paying attention to when the mouse moves
  AddListener(document, "mousemove", MouseMove);
  m.mouseDown = true;

  event.preventDefault ? event.preventDefault() : event.returnValue = false;
});

/**
 * mouseup event handler
 */
AddListener(document, "mouseup", function() {
  if (m.mouseDown) {
    var handler = MouseMove;
    if (document.detachEvent) {
      document.detachEvent("onmousemove", document["mousemove" + handler]);
      document["mousemove" + handler] = null;
    } else {
      document.removeEventListener("mousemove", handler, false);
    }

    m.mouseDown = false;

    if (m.mouseLocations.length > 0) {
      var clickCount = m.mouseLocations.length;
      m.velocity.x = (m.mouseLocations[clickCount - 1].x - m.mouseLocations[0].x) / clickCount;
      m.velocity.y = (m.mouseLocations[clickCount - 1].y - m.mouseLocations[0].y) / clickCount;
      m.mouseLocations.length = 0;
    }
  }

  m.viewingBox.style.cursor = m.hoverCursor;
});

drawMap();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="500" height="500">
</canvas>
<fieldset>
  <legend>Height Map Properties</legend>
  <input type="text" name="iterations" id="iterations">
  <label for="iterations">
    Iterations(7)
  </label>
  <label>
    <input type="checkbox" id="colors" />Rainbow</label>
</fieldset>

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.