프레넬(Fresnel)

아래와 같은 glsl 코드가 있을때 …

vec3 viewDirection = normalize(vPosition - cameraPosition);
vec3 normal = normalize(vNormal);
float fresnel = ...
vec3 color = vec3(fresnel);

gl_FragColor = vec4(color, 1.0);

세번째 줄의 코드를 다음처럼 하면 …

float fresnel = pow(dot(viewDirection, normal) + 1., 10.);

또는 다음처럼 하면 …

float fresnel = pow(abs(dot(viewDirection, normal) + .3), 2.);

물리적 광원에 대한 개별 노드 지정 (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>

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

다중 파일의 드랍에 대한 안전한 처리

웹 API가 계속 발전하고 그에 맞춰 Javascript도 개선되는 과정에서 괴리가 발생하는데요. 그중 한가지 사례로 다중 파일에 대한 드랍처리입니다.

아래의 드랍 처리는 2개 이상의 파일을 드랍했을때 처리되어야할 파일이 누락됩니다.

dropArea.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropArea.classList.remove('highlight');
  fileListElement.innerHTML = '';

  const items = e.dataTransfer.items;
  if (items) {
    for (let i = 0; i < items.length; i++) {
      try {
        const handle = await items[i].getAsFileSystemHandle();

        if (handle.kind === 'file') {
          const file = await handle.getFile();
          displayFile(file);
        } else if (handle.kind === 'directory') {
          await traverseFileSystemHandle(handle);
        }
      } catch (error) {
        console.error(error);
      }
    }
  }
});

위 코드의 문제점에 대한 원인은 비동기 처리를 위해 js에서는 for await .. of 문이 도입되었으나 아직 drop 이벤트 객체의 dataTransfer.items 컬렉션은 이를 지원하지 않기 때문입니다. 차선책으로 이를 개선한 코드가 다음과 같습니다.

dropArea.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropArea.classList.remove('highlight');

  fileListElement.innerHTML = '';

  const items = e.dataTransfer.items;
  if (items) {
    const processingPromises = [];

    for (let i = 0; i < items.length; i++) {
      processingPromises.push((async () => {
        if (items[i].getAsFileSystemHandle) {
          try {
            const handle = await items[i].getAsFileSystemHandle();
            if (handle.kind === 'file') {
              const file = await handle.getFile();
              displayFile(file);
            } else if (handle.kind === 'directory') {
              await traverseFileSystemHandle(handle);
            }
          } catch (error) {
            console.warn(error);
          }
        }
      })()); // 즉시 실행
    }
    await Promise.allSettled(processingPromises);
  }
});

Compression Streams API를 이용한 텍스트 데이터에 대한 압축 및 해제

Compression Streams API는 웹 표준 API로 데이터에 대한 압축 및 해제를 위한 API이다. 파일 단위가 아닌 데이터에 대한 압축으로 그 목적이 zip과는 다르지만 데이터 압축에 대한 알고리즘은 유사하다. 즉, zip은 여러개의 파일을 압축하면서 압축 해제시에는 파일 명과 디렉토리 구조까지 복원해주지만 Compression Sterams API는 데이터에 대한 압축으로 압축을 해제하면 해제된 데이터가 나오며 그 데이터를 하나의 파일로 저장한다.

Compression Streams API로 압축할 수 있는 데이터는 텍스트 든 바이너리든 모두 가능한데, 아래의 코드는 텍스트에 대한 압축과 압축된 데이터를 다시 원래의 텍스트 데이터로 복원(압축 해제)하는 코드이다.

async function compressAndDecompressString(text) {
  // 1. 텍스트를 Uint8Array로 변환
  const textEncoder = new TextEncoder();
  const encodedData = textEncoder.encode(text);

  console.log("원본 데이터:", text);
  console.log("원본 데이터 크기 (바이트):", encodedData.byteLength);

  // 2. 데이터를 압축 (gzip 형식)
  // ReadableStream.from()은 Node.js에서 사용 가능하며, 
  // 브라우저에서는 ArrayBuffer를 직접 ReadableStream으로 변환해야 함.
  // 여기서는 편의를 위해 Blob을 사용하여 ReadableStream을 생성합니다.
  const originalStream = new Blob([encodedData]).stream();
  const compressedStream = originalStream.pipeThrough(new CompressionStream("gzip"));

  // 3. 압축된 데이터를 ArrayBuffer로 읽기
  const compressedResponse = new Response(compressedStream);
  const compressedBuffer = await compressedResponse.arrayBuffer();

  console.log("압축된 데이터 크기 (바이트):", compressedBuffer.byteLength);
  console.log("압축률:", 
    ((1 - compressedBuffer.byteLength / encodedData.byteLength) * 100).toFixed(2) + "%");

  // 4. 압축된 데이터를 압축 해제
  const decompressedStream = new Blob([compressedBuffer]).stream()
    .pipeThrough(new DecompressionStream("gzip"));

  // 5. 압축 해제된 데이터를 ArrayBuffer로 읽기
  const decompressedResponse = new Response(decompressedStream);
  const decompressedBuffer = await decompressedResponse.arrayBuffer();

  // 6. 압축 해제된 Uint8Array를 다시 텍스트로 변환
  const textDecoder = new TextDecoder();
  const decompressedText = textDecoder.decode(decompressedBuffer);

  console.log("압축 해제된 데이터:", decompressedText);
  console.log("압축 해제된 데이터 크기 (바이트):", decompressedBuffer.byteLength);

  // 원본 데이터와 압축 해제된 데이터가 동일한지 확인
  console.log("원본과 압축 해제된 데이터 일치 여부:", text === decompressedText);
}

// 예제 실행
const longText = `The quick brown fox jumps over the lazy dog.`;

compressAndDecompressString(longText);