지오서비스웹의 지오코딩 결과에 PNU 코드 정보가 제공됩니다.

지오서비스웹에서 두번째로 가장 많이 사용되는 기능이 지오코딩입니다. 그간 받은 피드백 중 지오코딩 결과에 좌표 뿐만 아니라 PNU 코드가 필요하다는 이용자 분들이 계셨고 이에 대한 내용을 반영하였습니다.

PNU 코드는 지번주소에 대한 코드인데, 특히 도로명주소에 대한 PNU 코드도 제공될 수 있도록 하였습니다. PNU 코드는 다음과 같이 활용할 수 있습니다.

  • 지적도 및 GIS 데이터 연계: PNU 코드는 지적도, 도시계획, 부동산 데이터 등에서 필지(토지)를 정확히 식별하는 데 사용됩니다. 예를 들어, QGIS 등 GIS 소프트웨어에서 PNU 코드를 필터로 활용해 특정 토지를 조회하거나 지도상에서 시각화할 수 있습니다.
  • 주소 변환 및 데이터 전처리: 도로명주소, 지번주소 등 다양한 주소 형식을 PNU 코드로 변환하거나, 실거래가·부동산 데이터에서 PNU 코드를 추출해 분석에 활용합니다.
  • 행정·공공기관 시스템 연계: PNU 코드는 행정표준코드관리시스템 등에서 법정동, 읍면동, 지번 등 상세 정보를 조회하는 데도 활용됩니다.

사용자 인터페이스는 크게 달라진 부분은 없습니다. PNU 라는 체크 박스를 하나 추가해서 체크되면 PNU 코드값도 함께 저장되도록 하였습니다. PNU 저장은 기본적으로 체크되어 있습니다.

위의 내용처럼 설정하고 지오코딩을 실행한 결과를 들여다보면 다음처럼 _PNU라는 이름의 컬럼이 보이고 19자리의 코드값이 저장된 것을 확인할 수 있습니다.

TSL Tips

normalNode에 대한 적용을 위해 bumpMap 노드를 사용

const diffuseTex = loader.load('./brick_diffuse.jpg');
diffuseTex.colorSpace = THREE.SRGBColorSpace;

const bumpTex = loader.load('./brick_bump.jpg');

const wallMat = new THREE.MeshStandardNodeMaterial();

wallMat.colorNode = texture(diffuseTex);
wallMat.normalNode = bumpMap(texture(bumpTex), float(5));

각도와 거리에 대한 가하학적 계산식

GPU를 통해 쓰고 읽을 수 있는 텍스쳐

/* 필요한 API 임포트 */
import { texture, textureStore, Fn, instanceIndex, float, uvec2, vec4, time } from 'three/tsl';

/* 스토리지 텍스쳐 생성 */
const width = 512, height = 512;
const storageTexture = new THREE.StorageTexture(width, height);

/* 텍스쳐 내용 생성 함수 정의 */
const computeTexture = Fn(({ storageTexture }) => {
  const posX = instanceIndex.mod(width);
  const posY = instanceIndex.div(width);
  const indexUV = uvec2(posX, posY);

  const x = float(posX).div(50.0);
  const y = float(posY).div(50.0);

  const v1 = x.sin();
  const v2 = y.sin();
  const v3 = x.add(y.add(time.mul(1))).sin();
  const v4 = x.mul(x).add(y.mul(y)).sqrt().add(5.0).sin();
  const v = v1.add(v2, v3, v4);

  const r = v.sin();
  const g = v.add(Math.PI).sin();
  const b = v.add(Math.PI).sub(0.5).sin();

  textureStore(storageTexture, indexUV, vec4(r, g, b, 1)).toWriteOnly();
});

/* 텍스쳐 내용 생성 함수 실행 */
const computeNode = computeTexture({ storageTexture }).compute(width * height);
this._renderer.compute(computeNode);

/* 텍스쳐 활용 */
const material = new THREE.MeshBasicNodeMaterial({ color: 0x00ff00 });
material.colorNode = texture(storageTexture);

여러 개의 텍스쳐 데이터를 한꺼번에 쉐이더로 전달하기

텍스처는 이미지 그 이상의 가치를 가진 데이터이다. 텍스쳐를 쉐이더로 넘길때 흔히 하나씩 넘기는 경우가 흔한데, 가능하다면 한꺼번에 넘기는게 속도면에서 훨씬 이득이다. 즉, 텍스쳐 배열 타입(sampler2DArray)으로 쉐이더에서 받도록 한다. 이를 위해 three.js 개발자는 다음과 같은 편리한 클래스를 제공한다.

class TextureAtlas {
  // 너무 길어서 전체 코드는 이 글 맨 아래 참조
}

사용 방법을 보자. 먼저 전달할 여러개의 이미지 파일을 위의 클래슬르 통해 불러온다.

const textures = new TextureAtlas();
diffuse.Load('myTextures', [
  './textures/a.png',
  './textures/b.png',
  './textures/c.png',
  ...
]);

textures.onLoad = () => {
  myShaderMaterial.uniforms.uniformData.value = textures.Info['myTextures'].atlas;
};

쉐이더 코드에서는 uniformData라는 이름의 uniform 데이터를 다음처럼 참조할 수 있게 된다.

uniform sampler2DArray uniformData;

main() {
  vec4 color = texture2D(uniformData, vec3(uv, 0.0));

  ...
}

texture2D의 2번째 인자가 3차원 데이터인데, 3번째 차원의 값을 통해 어떤 텍스처 데이터를 사용할지를 지정하는 인덱스이다. 0이면 첫번째 텍스쳐 데이터인 a.png, 2이면 c.png라는 식이다. 쉽죠?

너무 길어 보여주지 않았던 TextureAtlas의 전체 코드는 다음과 같다.

function _GetImageData(image) {
  const canvas = document.createElement('canvas');
  canvas.width = image.width;
  canvas.height = image.height;

  const context = canvas.getContext('2d');
  context.translate(0, image.height);
  context.scale(1, -1);
  context.drawImage(image, 0, 0);

  return context.getImageData( 0, 0, image.width, image.height );
}

class TextureAtlas {
  constructor() {
    this.create_();
    this.onLoad = () => {};
  }

  Load(atlas, names) {
    this.loadAtlas_(atlas, names);
  }

  create_() {
    this.manager_ = new THREE.LoadingManager();
    this.loader_ = new THREE.TextureLoader(this.manager_);
    this.textures_ = {};

    this.manager_.onLoad = () => {
      this.onLoad_();
    };
  }

  get Info() {
    return this.textures_;
  }

  onLoad_() {
    for (let k in this.textures_) {
      let X = null;
      let Y = null;
      const atlas = this.textures_[k];
      let data = null;

      for (let t = 0; t < atlas.textures.length; t++) {
        const loader = atlas.textures[t];
        const curData = loader();

        const h = curData.height;
        const w = curData.width;

        if (X === null) {
          X = w;
          Y = h;
          data = new Uint8Array(atlas.textures.length * 4 * X * Y);
        }

        if (w !== X || h !== Y) {
          console.error('Texture dimensions do not match');
          return;
        }
        const offset = t * (4 * w * h);

        data.set(curData.data, offset);
      }

      const diffuse = new THREE.DataArrayTexture(data, X, Y, atlas.textures.length);
      diffuse.format = THREE.RGBAFormat;
      diffuse.type = THREE.UnsignedByteType;
      diffuse.minFilter = THREE.LinearMipMapLinearFilter;
      diffuse.magFilter = THREE.LinearFilter;
      diffuse.wrapS = THREE.ClampToEdgeWrapping;
      diffuse.wrapT = THREE.ClampToEdgeWrapping;
      // diffuse.wrapS = THREE.RepeatWrapping;
      // diffuse.wrapT = THREE.RepeatWrapping;
      diffuse.generateMipmaps = true;
      diffuse.needsUpdate = true;

      atlas.atlas = diffuse;
    }

    this.onLoad();
  }

  loadType_(t) {
    if (typeof(t) == 'string') {
      const texture = this.loader_.load(t);
      return () => {
        return _GetImageData(texture.image);
      };
    } else {
      return () => {
        return t;
      };
    }
  }

  loadAtlas_(atlas, names) {
    this.textures_[atlas] = {
      textures: names.map(n => this.loadType_(n))
    };
  }
}

뭐.. 별거 없죠?

베지어(bezier)와 기울기

베지어 함수의 코드는 다음과 같다.

vec3 bezier(vec3 P0, vec3 P1, vec3 P2, vec3 P3, float t) {
  float u = 1.0 - t;
  float tt = t * t;
  float uu = u * u;
  float uuu = uu * u;
  float ttt = tt * t;

  vec3 p = uuu * P0; // (1-t)^3 * P0
  p += 3.0 * uu * t * P1; // 3*(1-t)^2*t*P1
  p += 3.0 * u * tt * P2; // 3*(1-t)*t^2*P2
  p += ttt * P3; // t^3*P3

  return p;
}

베지어 상의 접선에 대한 함수 코드는 다음과 같다.

vec3 bezierGrad(vec3 P0, vec3 P1, vec3 P2, vec3 P3, float t) {
  return 3.0 * (1.0 - t) * (1.0 - t) * (P1 - P0) +
    6.0 * (1.0 - t) * t * (P2 - P1) +
    3.0 * t * t * (P3 - P2);
}

접선에 대한 벡터를 90도 회전하면 베지어의 법선 벡터를 구할 수 있다.