2개의 모양 함수를 smooth하게 섞는 방법(smooth min/max)

위에서 a와 b는 모양 함수(Shape Function)인데, a와 b의 값에 대한 min 또는 max 값을 취하면 a와 b를 하나로 섞을 수 있다. 그런데 a와 b의 교차점에서 매우 날카롭게 섞이게 되는데, 이를 부드럽게 섞은 것이 세번째 함수의 결과이다. 세번째 함수를 보면 k, h, m으로 구성되는데 이 중 k의 값에 따라 얼마나 부드럽게 섞을 것인지 결정한다. k 값이 음수일때 max 값으로 섞고 양수일때 min 값으로 섞는다.

float smin(float a, float b, float k) {
  float h = clamp(.5 + .5 * (b - a) / k, 0., 1.);
  return mix(b, a, h) - k * h * (1. - h);
}

Ray-Sphere 계산

수식

코드

uniform vec3 uResolution;
uniform float uTime;
uniform vec4 uMouse;

// v = a -> 0
// v = b -> 1 
// v = (a+b)/2 -> 0.5
float remap01(float a, float b, float v) {
  return (v - a) / (b - a);
}

void main() {
  vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;

  vec3 col = vec3(0);
  vec3 Ro = vec3(0);
  vec3 Rd = normalize(vec3(uv.x, uv.y, 1.));
  vec3 S = vec3(0, 0, 3);
  float R = 1.;

  float tp = dot(S - Ro, Rd);
  vec3 Ptp = Ro + Rd * tp;
  float y = length(S - Ptp);
  
  if(y < R) {
    float x = sqrt(R*R - y*y);
    float t1 = tp - x;
    float t2 = tp + x;

    float c = remap01(S.z, S.z - R, t1);
    col = vec3(c);
  }
  
  gl_FragColor = vec4(col, 1.0);
}

결과

code snippet (20250417,21)

  // if(p.x > .5 || p.y > .5) col.r = .8;
  if(max(p.x, p.y) > .5) col.r = .8;
  // col.r = step(.5, max(p.x, p.y));
  float a = radians(45.);
  vec2 n = vec2(sin(a), cos(a));
  float d = dot(uv, n);
  d = abs(d);
  col += smoothstep(fwidth(d), 0., d-.001);
function prepareOneFish() {
  let start = new Date().getTime();
  while (new Date().getTime() < start + 1000) {
    // preparing fish
  }
}
function setTimeoutPromise(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}

function setTimeoutPromise(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

three.js 웹을 Electron을 이용해 어플리케이션으로 만들기 (1/3)

웹은 웹브라우저를 통해 URL만 알면 어디서든 사용할 수 있는 접근성 ‘갑’ 기술이다. 그러나 이 웹은 치명적인 단점이 있는데 바로 인터넷이 되는 환경이여야 하고 또 기본적으로 로컬 리소스에 자유롭게 접근할 수 없다는 점이다. (물론 웹에서도 사용자의 인터렉션을 통해 로컬 리소스을 접근할 수 있기는 하다.)

아래의 동영을 보자. three.js를 이용해 간단한 3D 뷰 페이지를 만들었고 내 PC에 저장된 모델 파일을 열어 표시한다. 분명 웹으로 만들었지만 이 프로그램의 타이틀을 보면 크롬등과 같은 프로그램이 아닌 독자적인 프로그램이라는 것을 알 수 있다. 이 프로그램은 원래 웹이였던 아이를 Electron이라는 기술을 이용해 인터넷 없이도 실행할 수 있는 단독 실행 파일로 만들어 낸 결과이다.

위의 결과를 만들기 위한 방법을 유튜브 영상으로 만들어 올릴 예정인데, 그전에 블로그에 정리하기 위한 목적으로 이 글을 작성한다.

먼저 간단한 웹페이지를 만들어야 한다. 나는 다음 git 명령을 통해 이미 작성된 프로젝트를 가져왔다.

git clone https://github.com/GISDEVCODE/threejs-with-javascript-starter.git .

패키지를 설치하고 실행까지해보면 다음과 같은 결과를 볼 수 있다.

이 실행 결과를 배포 버전으로 만들어야 한다. Electron은 웹 페이지에 대해 배포 버전을 대상으로 하기 때문이다. 이 프로젝트의 경우 배포 명령은 다음과 같다.

npm run build

위의 명령을 실행하기에 앞서 아래 글을 참고해서 손이 덜 가도록 만드는 것을 추천한다.

Vite로 개발된 웹앱 배포 시 주의점

배포 버전을 만들면 dist라는 폴더에 그 결과가 생성되고 이 dist 폴더의 결과를 Electron으로 단독 실행이 가능한 프로그램으로 만들 수 있다. 이를 위해 다음 명령으로 필요한 패키지를 설치한다.

npm i electron --save-dev

Electron에만 관련된 소스코드를 따로 관리하기 위해 프로젝트에서 electron 폴더를 만들고 electron-run.cjs와 preload.js 파일을 만든다. 확장자가 각각 cjs와 js라는 점에 유의하자. electron-run.cjs의 코드를 다음처럼 입력한다.

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

let win = null;
const createWindow = () => {
  win = new BrowserWindow({
    width: 1024,
    height: 768,
    webPreferences: {
      nodeIntegration: true,
      preload: path.resolve("electron/preload.js")
    }
  });

  win.loadFile(`${path.join(__dirname, '../dist/index.html')}`);
};

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

package.json에 위의 electron-run.cjs를 참조를 다음처럼 해준다.

{
  "main": "./electron/electron-run.cjs",
  ...

  "scripts": {
    ...
    "electron": "electron ."
  },

  ...
}

이제 기본적인 것은 모두 끝났다. 다음 명령을 통해 우리의 웹이 크롬과 같은 브라우저 없이도 단독 실행 가능한 어플리케이션으로 탄생시키기 위해 다음 명령을 실행한다.

npm run electron