물리적 광원에 대한 개별 노드 지정 (TSL 방식)

three.js 쉐이더 언어인 TSL에서 물리적 광원에 대한 개별 노드를 지정하는 방식을 정리해 봅니다. TSL을 통해 표현하고자 하는 재질은 다음과 같습니다.

코드는 다음과 같습니다.

const geometry = new THREE.IcosahedronGeometry(1, 64);
const material = new THREE.MeshStandardNodeMaterial({
  color: "black",
  // wireframe: true,
});
const mesh = new THREE.Mesh(geometry, material)

const path = './Metal053B_2K-JPG';

const texColor = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Color.jpg`);
material.colorNode = texture(texColor);

const texNormal = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_NormalGL.jpg`);
material.normalNode = normalMap(texture(texNormal), float(1.));

const texMetalness = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Metalness.jpg`);
material.metalnessNode = mul(texture(texMetalness), 1.);

const texRoughness = new THREE.TextureLoader().load(`${path}/Metal053B_2K-JPG_Roughness.jpg`);
material.roughnessNode = mul(texture(texRoughness), float(0.7));

광원 노드에 집중하기 위해서 텍스쳐 데이터를 사용했습니다. 텍스쳐 본연의 표현을 위해 재질의 기본 색상을 블랙으로 지정했구요. 사용한 노드는 colorNode, normalNode, metalnessNode, roughnessNode입니다.

광원에 대한 노드는 아니지만 Displacement에 대한 노드를 알아보기 위해 표현하고자 하는 재질은 다음과 같습니다.

코드는 다음과 같습니다.

const geometry = new THREE.IcosahedronGeometry(1, 64);
const material = new THREE.MeshStandardNodeMaterial({
  color: "black",
  // wireframe: true,
});
const mesh = new THREE.Mesh(geometry, material)

const path = './Rock058_2K-JPG';

...

const texAO = new THREE.TextureLoader().load(`${path}/Rock058_2K-JPG_AmbientOcclusion.jpg`);
geometry.setAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2));
material.aoNode = mul(texture(texAO), float(1.));

const texDisplacement = new THREE.TextureLoader().load(`${path}/Rock058_2K-JPG_Displacement.jpg`);
const displacementNode = texture(texDisplacement);
const displaceStrength = 0.3;
const displacementValue = displacementNode.r.sub(0.5).mul(displaceStrength);
const newPosition = positionWorld.add(normalLocal.mul(displacementValue));
material.positionNode = newPosition;

Displacement 표현을 위해서는 Vertex Shader에 해당하는 positionNode를 이용해야 합니다. 추가로 AO 노드에 대한 사용 코드도 보입니다.

알고 있으면 너무 좋은 프론트엔드 웹 기술

웹은 인류가 만든 최고의 문화 또는 기술 중 하나입니다. 웹을 통해 이룰 수 있는 그 가능성은 무한하며 그 가능성을 현실로 이루기 위해서는 기술이 필요한데, 이러한 기술에 대해 설명합니다.

#Compression Stream API

#Screen Capture API

#Encoding API

#Web Speech API

#Broadcast Channel API

#Drag and Drop Multi Files / Folder

#File System Access API

#Cross Orgin Communication API

#Web Crypto API

#Web Storage API

#WebAssembly

#Web Component API

#Screen Wake Lock API

#Beacon API

#WebRTC

#CSS Custom Highlight API

#Channel Messaging API

#View Transitions API

#WebWorker (Dedicated Worker)

#IndexedDB API

#WebSocket

#Web Audio API

#Notifications API

#Prioritized Task Scheduling API/h2>

위의 영상에 더해 더 많은 영상을 제공하고 지속적으로 내용이 추가되므로 해당 채널에 방문하여 참고하시기 바랍니다.

PWA (Progressive Web APP) 개발 가이드

vite로 구성된 바닐라 프로젝트가 기준이다. 먼저 index.html 파일에 manifest.json에 대한 연결이 필요하다.

<head>
  <link rel="manifest" href="/manifest.json" />
  <link rel="icon" href="/icon_192x192.png" />
</head>

위의 코드를 보면 선택사항이지만 아이콘도 지정했다. 이 아이콘 역시 manifest 내부에서 언급되는데, index.html은 url을 통해 접속했을때 웹브라우저에 표시될 아이콘이고 manifest에서는 바탕화면 등에 App으로 등록될때 표시되는 아이콘이다. mainfest.json 파일은 다음과 같으며 위치는 public 폴더이다.

{
  "name": "Vite PWA 예제",
  "short_name": "VitePWA",
  "description": "간단한 Vite 기반 PWA 예제",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ff0000",
  "background_color": "#ff0000",
  "icons": [
    {
      "src": "/icon_192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon_512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

여기까지만 해도 PWA로써 내가 원하는 아이콘과 제목 등으로 설정된 앱으로 설치할 수 있게 된다. 여기에 더해…. main.js 파일에 대한 내용이다. 크게 3가지 내용이다. DOM 구성, 서비스워커 구성, 앱 설치 UI이다. 먼저 DOM 구성은 다음처럼 했다.

import './style.css'

document.querySelector('#app').innerHTML = /*html*/ `
  

Vite PWA 예제

이것은 사용자 정의 Service Worker를 사용한 PWA입니다.

오프라인에서도 동작하며, 설치 가능합니다!

`;

서비스 워크 구성은 다음처럼 했다.

window.addEventListener('load', () => {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 등록 성공:', registration.scope);
    })
    .catch(error => {
      console.error('Service Worker 등록 실패:', error);
    });
});

앱 설치 UI에 대한 코드는 다음처럼 했다.

let deferredPrompt;
//const installButton = document.getElementById('installButton');
//const installMessage = document.getElementById('installMessage');

window.addEventListener('beforeinstallprompt', (e) => {
  // e.preventDefault();
  deferredPrompt = e;
  installButton.style.display = 'block';
  installMessage.textContent = '앱을 바탕화면에 설치할 수 있습니다!';
});

installButton.addEventListener('click', async () => {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    installMessage.textContent = outcome === 'accepted'
      ? '앱 설치가 시작되었습니다!'
      : '앱 설치가 취소되었습니다.';
    deferredPrompt = null;
    installButton.style.display = 'none';
  }
});

window.addEventListener('appinstalled', () => {
  installMessage.textContent = '앱이 성공적으로 설치되었습니다!';
  installButton.style.display = 'none';
});

서비스워커에 대한 sw.js 파일의 코드는 다음과 같으며 파일 위치는 public이다. 이 서비스워커는 캐쉬에 대한 기능이다.

const CACHE_NAME = 'vite-pwa-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/style.css',
  '/icon_192x192.png',
  '/icon_512x512.png'
];

// 설치 시 캐싱
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('캐시 열기 성공');
        return cache.addAll(urlsToCache);
      })
  );
});

// 요청 처리 (CacheFirst 전략)
self.addEventListener('fetch', event => {
  const dest = event.request.destination;
  // if (event.request.destination === 'document') {
    event.respondWith(
      caches.match(event.request)
        .then(cachedResponse => {
          // 캐시에 있으면 캐시된 응답 반환
          if (cachedResponse) {
            console.log(dest + ' (' + event.request.url + ') from cache');
            return cachedResponse;
          }
          // 캐시에 없으면 네트워크 요청
          return fetch(event.request)
            .then(networkResponse => {
              // 네트워크 응답을 캐시에 저장
              console.log(dest + ' (' + event.request.url + ') from fetch');
              return caches.open(CACHE_NAME).then(cache => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
            })
            .catch(() => {
              onsole.log(dest + ' (' + event.request.url + ') failed');
              // 네트워크 요청 실패 시 (오프라인 등)
              return new Response('오프라인 상태입니다. 캐시도 없습니다.', {
                status: 503,
                statusText: 'Service Unavailable'
              });
            });
        })
    );
  // }
});

// 요청 처리 (NetworkFirst 전략)
self.addEventListener('_____fetch', event => {
  console.log(event.request.destination);
  // if (event.request.destination === 'document') {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // 네트워크 응답을 캐시에 저장
          return caches.open(CACHE_NAME).then(cache => {
            console.log(event.request.url + ' from fetch');
            cache.put(event.request, response.clone());
            return response;
          });
        })
        .catch(() => {
          // 네트워크 실패 시 캐시에서 가져오기
          console.log(event.request.url + ' from cache');
          return caches.match(event.request);
        })
    );
  // }
});

// 캐시 정리
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  console.log('캐시 정리 성공');
});

Push 기능을 위한 Key 생성

npx web-push generate-vapid-keys

아래와 같은 예시로 그 결과가 출력됨.

=======================================

Public Key:
BE1ZFx8L3zod6eVQsp_esYMinG_C5A3CA9w0rKqwwKDyfcMmMHpJpJm0HB4Usp3gnnLi3sQz5exFbGZiNIBBjJk

Private Key:
9KyZMbnDTVhDnabu8xxRt1tSHNvddskvnT3lBwbYEkI

=======================================

Public Key는 클라이언트에, Public Key + Private Key는 서버에 지정된다.

참고로 Push 기능 테스트를 위해 url을 호출해야 하는데 curl을 이용하면 다음과 같다.

curl -X GET http://localhost:3000/send-push