Codrops 원본

Animating 160,000 Cubes in Three.js to Visualize Dithering

섹션 · MIT

섹션 더 보기
ORIGINAL PREVIEW
Animating 160,000 Cubes in Three.js to Visualize Dithering 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

11개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

index.html
파일 저장

<!DOCTYPE html>
<html lang="en" class="no-js">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Visualizing Dithering Process with Three.js | Codrops</title>
  <link rel="icon" type="image/svg+xml" href="https://tympanus.net/favicon/favicon.svg" />
  <link rel="shortcut icon" href="https://tympanus.net/favicon/favicon.ico" />
  <script>
    document.documentElement.className = 'js';
  </script>
</head>

<body class="demo-1">
  <main>
    <header class="frame">
      <h1 class="frame__title">Visualizing Dithering Process with Three.js</h1>
      <a class="frame__back" href="https://tympanus.net/codrops/?p=108906">Article</a>
      <a class="frame__archive" href="https://tympanus.net/codrops/hub/">All demos</a>
      <a class="frame__github" href="https://github.com/damarberlari/visualizing-dithering-codrops/">GitHub</a>
      <nav class="frame__tags">
        <a href="https://tympanus.net/codrops/hub/tag/three-js">#three.js</a>
        <a href="https://tympanus.net/codrops/hub/tag/webgl">#webgl</a>
        <a href="https://tympanus.net/codrops/hub/tag/dithering">#dithering</a>
      </nav>
    </header>
    <div class="content">
      <canvas class="webgl"></canvas>
    </div>
  </main>

  <script type="module" src="/src/index.js"></script>
</body>

</html>
src/Grid/Grid.js
파일 저장

import * as THREE from "three";
import vertexShader from "./shaders/vertexShader.glsl";
import fragmentShader from "./shaders/fragmentShader.glsl";

class Grid {
  constructor(gridProperties) {
    this.drawn = false; // Whether the grid has been initialized and drawn
    this.shown = false; // Whether the grid is currently added to the scene

    this.gridProperties = gridProperties;

    this.thresholdMaps = [
      {
        id: "bayer4x4",
        name: "Bayer 4x4",
        rows: 4,
        columns: 4,
        data: [
          0, 8, 2, 10,
          12, 4, 14, 6,
          3, 11, 1, 9,
          15, 7, 13, 5
        ]
      },
      {
        id: "halftone",
        name: "Halftone",
        rows: 8,
        columns: 8,
        data: [
          24, 10, 12, 26, 35, 47, 49, 37,
          8, 0, 2, 14, 45, 59, 61, 51,
          22, 6, 4, 16, 43, 57, 63, 53,
          30, 20, 18, 28, 33, 41, 55, 39,
          34, 46, 48, 36, 25, 11, 13, 27,
          44, 58, 60, 50, 9, 1, 3, 15,
          42, 56, 62, 52, 23, 7, 5, 17,
          32, 40, 54, 38, 31, 21, 19, 29
        ]
      },
      {
        id: "bayer8x8",
        name: "Bayer 8x8",
        rows: 8,
        columns: 8,
        data: [
          0, 32, 8, 40, 2, 34, 10, 42,
          48, 16, 56, 24, 50, 18, 58, 26,
          12, 44, 4, 36, 14, 46, 6, 38,
          60, 28, 52, 20, 62, 30, 54, 22,
          3, 35, 11, 43, 1, 33, 9, 41,
          51, 19, 59, 27, 49, 17, 57, 25,
          15, 47, 7, 39, 13, 45, 5, 37,
          63, 31, 55, 23, 61, 29, 53, 21
        ]
      },
      {
        id: "voidAndCluster",
        name: "Void and Cluster",
        rows: 14,
        columns: 14,
        data: [
          131, 187, 8, 78, 50, 18, 134, 89, 155, 102, 29, 95, 184, 73,
          22, 86, 113, 171, 142, 105, 34, 166, 9, 60, 151, 128, 40, 110,
          168, 137, 45, 28, 64, 188, 82, 54, 124, 189, 80, 13, 156, 56,
          7, 61, 186, 121, 154, 6, 108, 177, 24, 100, 38, 176, 93, 123,
          83, 148, 96, 17, 88, 133, 44, 145, 69, 161, 139, 72, 30, 181,
          115, 27, 163, 47, 178, 65, 164, 14, 120, 48, 5, 127, 153, 52,
          190, 58, 126, 81, 116, 21, 106, 77, 173, 92, 191, 63, 99, 12,
          76, 144, 4, 185, 37, 149, 192, 39, 135, 23, 117, 31, 170, 132,
          35, 172, 103, 66, 129, 79, 3, 97, 57, 159, 70, 141, 53, 94,
          114, 20, 49, 158, 19, 146, 169, 122, 183, 11, 104, 180, 2, 165,
          152, 87, 182, 118, 91, 42, 67, 25, 84, 147, 43, 85, 125, 68,
          16, 136, 71, 10, 193, 112, 160, 138, 51, 111, 162, 26, 194, 46,
          174, 107, 41, 143, 33, 74, 1, 101, 195, 15, 75, 140, 109, 90,
          32, 62, 157, 98, 167, 119, 179, 59, 36, 130, 175, 55, 0, 150
        ]
      },
    ]

    this.cellProperties = this.calculateCellProperties(gridProperties);
  }

  calculateCellProperties(gridProperties) {
    const rowCount = gridProperties.rowCount || 1;
    const columnCount = gridProperties.columnCount || 1;
    const cellSpacing = gridProperties.cellSize || 1;

    const objectCount = rowCount * columnCount;
    const properties = new Array(objectCount);


    for (let i = 0; i < objectCount; i++) {
      properties[i] = {};
      properties[i].cellIdNormalized = i / (objectCount - 1); // Normalize cellId to [0, 1] range

      const rowId = Math.floor(i / columnCount);
      const columnId = i % columnCount;

      // Place the cell on the grid based on its row and column, then centering the grid around the origin
      const x = (columnId - (columnCount - 1) / 2) * cellSpacing;
      const y = (-rowId + (rowCount - 1) / 2) * cellSpacing;
      const z = 0;

      // Store the calculated position in the properties array
      properties[i].x = x;
      properties[i].y = y;
      properties[i].z = z;
      properties[i].rowIdNormalized = rowId / (rowCount - 1);
      properties[i].columnIdNormalized = columnId / (columnCount - 1);
      properties[i].thresholdMaps = {}; // Prepare an object to hold threshold map values for this cell

      this.thresholdMaps.forEach(config => {
            const { data, rows: matrixRowSize, columns: matrixColumnSize } = config;
            const matrixSize = data.length;
            const matrixRow = rowId % matrixRowSize;
            const matrixColumn = columnId % matrixColumnSize;
            const index = matrixColumn + matrixRow * matrixColumnSize;

            const thresholdValue = data[index] / matrixSize; // Normalize threshold to [0, 1]

            properties[i].thresholdMaps[config.id] = thresholdValue;
      });

    }

    return properties;
  }

  init() {
    const cellSize = this.gridProperties.cellSize || 1;
    const cellThickness = this.gridProperties.cellThickness || 1;

    const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellThickness);

    const attributes = {
      aCellIdNormalized: new THREE.InstancedBufferAttribute(
        new Float32Array(this.cellProperties.map((prop) => prop.cellIdNormalized)),
        1
      ),
      aRowIdNormalized: new THREE.InstancedBufferAttribute(
        new Float32Array(this.cellProperties.map((prop) => prop.rowIdNormalized)),
        1
      ),
      aColumnIdNormalized: new THREE.InstancedBufferAttribute(
        new Float32Array(this.cellProperties.map((prop) => prop.columnIdNormalized)),
        1
      ),
      aDitheringThresholds: {} // Prepare an object to hold threshold map attributes
    };

    this.thresholdMaps.forEach(config => {
      attributes.aDitheringThresholds[config.id] = new THREE.InstancedBufferAttribute(
        new Float32Array(this.cellProperties.map((prop) => prop.thresholdMaps[config.id])),
        1
      );
    });


    geometry.setAttribute("aCellIdNormalized", attributes.aCellIdNormalized);
    geometry.setAttribute("aRowIdNormalized", attributes.aRowIdNormalized);
    geometry.setAttribute("aColumnIdNormalized", attributes.aColumnIdNormalized);
    geometry.setAttribute("aDitheringThreshold", attributes.aDitheringThresholds.bayer4x4); // Using bayer4x4 as the default threshold map for now

    const material = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      defines: {
        DELAY_TYPE: 1,
        GRID_TYPE: this.gridProperties.gridType ?? 1,
      },
      uniforms: {
        uZPositionRange: { value: this.gridProperties.zPositionRange ?? new THREE.Vector2(0, 0) },
        uCellScaleRange: { value: this.gridProperties.cellScaleRange ?? new THREE.Vector2(1, 1) },
        uAnimationProgress: { value: 0 },
        uAnimationMinDelay: { value: this.gridProperties.animationMinDelay ?? 0 }, // Minimum delay for the animation in % of duration.
        uAnimationMaxDelay: { value: this.gridProperties.animationMaxDelay ?? 0.9 }, // Maxium delay for the animation in % of duration.
        uTexture: { value: null }, // Placeholder for texture uniform
      },
    });

    // Load image to material.uniforms.uTexture if the image path is provided
    if (this.gridProperties.image) {
      const textureLoader = new THREE.TextureLoader();
      textureLoader.load(
        this.gridProperties.image,
        (texture) => {
          texture.colorSpace = THREE.SRGBColorSpace;
          material.uniforms.uTexture.value = texture;
          material.needsUpdate = true;
        }
      );
    }

    const mesh = new THREE.InstancedMesh(
      geometry,
      material,
      this.cellProperties.length // Number of instances
    );

    //Update Cell Position and Size for each instance
    for (let i = 0; i < this.cellProperties.length; i++) {
      const { x, y, z } = this.cellProperties[i];

      const objectRef = new THREE.Object3D();
      objectRef.position.set(x, y, z);
      objectRef.updateMatrix();

      mesh.setMatrixAt(i, objectRef.matrix);
    }

    mesh.instanceMatrix.needsUpdate = true;

    // Create a group to hold the mesh
    const group = new THREE.Group();
    group.add(mesh);

    // Store references for later use
    this.group = group;
    this.geometry = geometry;
    this.attributes = attributes; // Store attributes for later use
    this.material = material;
    this.instance = mesh;
    this.drawn = true;
  }

  showAt(scene) {
    if (!this.drawn) {
      this.init();
    }

    if (!this.shown) {
      scene.add(this.group);
      this.shown = true;
    }
  }

  hideFrom(scene) {
    if (this.shown) {
      scene.remove(this.group);
      this.shown = false;
    }
  }
}

export default Grid;
함께 쓰는 파일 9개 보기
src/Grid/shaders/fragmentShader.glsl
파일 저장

varying vec3 vColor; // Varying to receive the color from the vertex shader

void main() {
    vec3 color = vColor; // Use the color passed from the vertex shader
    
    gl_FragColor = vec4(color, 1.0);
}
src/Grid/shaders/vertexShader.glsl
파일 저장

uniform vec2 uZPositionRange; // Range for z position animation (start and end)
uniform vec2 uCellScaleRange; // Range for cell scale animation (start and end)
uniform float uAnimationProgress; // Animation progress (0.0 to 1.0) to control the z position animation
uniform float uAnimationMinDelay; // Minimum delay for the animation.
uniform float uAnimationMaxDelay; // Maxium delay for the animation.
uniform sampler2D uTexture; // Texture uniform to sample the image color

attribute float aRowIdNormalized; // Normalized row ID attribute
attribute float aColumnIdNormalized; // Normalized column ID attribute
attribute float aCellIdNormalized; // Normalized cell ID attribute
attribute float aDitheringThreshold; // Dithering threshold attribute for each cell

varying vec3 vColor; // Varying to pass the color to the fragment shader

float random (vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))*
        43758.5453123);
}

void main() {
    //Calculate delay index based on the selected delay type
    #ifdef DELAY_TYPE
        #if DELAY_TYPE == 1
            // Cell Index - based delay
            float delayFactor = aCellIdNormalized;
        #elif DELAY_TYPE == 2
            // Row-based delay
            float delayFactor = aRowIdNormalized;
        #elif DELAY_TYPE == 3
            // Column-based delay
            float delayFactor = aColumnIdNormalized;   
        #elif DELAY_TYPE == 4
            // random-based delay
            float delayFactor = random(vec2(aColumnIdNormalized, aRowIdNormalized));
        #elif DELAY_TYPE == 5
            // delay based on distance from the top-left corner;
            float delayFactor = distance(vec2(aRowIdNormalized, aColumnIdNormalized), vec2(0, 0));
            delayFactor = smoothstep(0.0, 1.42, delayFactor);
        #else
            // No delay
            float delayFactor = 0.0;
        #endif
    #else
        // Default to no delay if DELAY_TYPE is not defined
        float delayFactor = 0.0;
    #endif
    
    float animationStart = mix(uAnimationMinDelay, uAnimationMaxDelay, delayFactor);
    float animationDuration = 1.0 - uAnimationMaxDelay;
    float animationEnd = animationStart + animationDuration;

    #ifdef GRID_TYPE
        #if GRID_TYPE == 1
            // Image Mode
            float imageColor = texture2D(uTexture, vec2(aColumnIdNormalized, 1.0 - aRowIdNormalized)).r;
            float ditheringThreshold = aDitheringThreshold;
            float ditheredColor = step(ditheringThreshold, imageColor);

            float colorAnimationStart = animationStart + animationDuration * 0.5; // Start color animation halfway through the z-position animation
            float colorAnimationEnd = colorAnimationStart + 0.01; // End color animation at the same time as z-position animation

            float colorAnimationProgress = smoothstep(colorAnimationStart, colorAnimationEnd, uAnimationProgress);

            float finalColor = mix(imageColor, ditheredColor, colorAnimationProgress);

            //Add border
            float borderThreshold = 0.005; // Adjust this value to control the thickness of the border
            float borderX = step(aColumnIdNormalized, borderThreshold) + step(1.0 - borderThreshold, aColumnIdNormalized);
            float borderY = step(aRowIdNormalized, borderThreshold) + step(1.0 - borderThreshold, aRowIdNormalized);
            float isBorder = clamp(borderX + borderY, 0.0, 1.0);
            finalColor = mix(finalColor, 0.0, isBorder);
        #elif GRID_TYPE == 2
            // Threshold Map Mode
            float finalColor = aDitheringThreshold;
        #else 
            // Solid Color Mode
            float finalColor = 0.5;
        #endif
    #else
        // Default to solid color mode if GRID_TYPE is not defined
        float finalColor = 0.5;
    #endif

    float cellScaleStart = uCellScaleRange.x;
    float cellScaleEnd = uCellScaleRange.y;
    float cellScaleAnimationProgress = smoothstep(animationStart, animationEnd, uAnimationProgress);
    float cellScale = mix(cellScaleStart, cellScaleEnd, cellScaleAnimationProgress);
    vec3 cellLocalPosition = vec3(position);
    cellLocalPosition *= cellScale;

    vec4 cellWorldPosition = modelMatrix * instanceMatrix * vec4(cellLocalPosition, 1.0);
    
    // Calculate the z position start and end position based on the uniform values
    float zPositionStart = uZPositionRange.x;
    float zPositionEnd = uZPositionRange.y;
    
    // Smoothen the z position animation progress using smoothstep
    // Animations will start at animationStart and end at animationEnd value for each cube
    float zPositionAnimationProgress = smoothstep(animationStart, animationEnd, uAnimationProgress);
    
    // Update the world z position of the cell based on the zPositionAnimationProgress value
    cellWorldPosition.z += mix(zPositionStart, zPositionEnd, zPositionAnimationProgress);

    gl_Position = projectionMatrix * viewMatrix * cellWorldPosition;

    vColor = vec3(finalColor);
}
src/css/base.css
파일 저장

*,
*::after,
*::before {
  box-sizing: border-box;
}

:root {
  font-size: 12px;
  --color-text: #fff;
  --color-bg: #000;
  --color-link: #fff;
  --color-link-hover: #fff;
  --page-padding: 1.5rem;
}

body {
  margin: 0;
  color: var(--color-text);
  background-color: var(--color-bg);
  font-family: ui-monospace, monospace;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@media (scripting: enabled) {
  .loading {
    &::before,
    &::after {
      content: '';
      position: fixed;
      z-index: 10000;
    }

    &::before {
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: var(--color-bg);
    }

    &::after {
      top: 50%;
      left: 50%;
      width: 100px;
      height: 1px;
      margin: 0 0 0 -50px;
      background: var(--color-link);
      animation: loaderAnim 1.5s ease-in-out infinite alternate forwards;
    }
  }
}

@keyframes loaderAnim {
  0% {
    transform: scaleX(0);
    transform-origin: 0% 50%;
  }

  50% {
    transform: scaleX(1);
    transform-origin: 0% 50%;
  }

  50.1% {
    transform: scaleX(1);
    transform-origin: 100% 50%;
  }

  100% {
    transform: scaleX(0);
    transform-origin: 100% 50%;
  }
}

a {
  text-decoration: none;
  color: var(--color-link);
  outline: none;
  cursor: pointer;

  &:hover {
    text-decoration: underline;
    color: var(--color-link-hover);
  }

  &:focus {
    outline: none;
    background: lightgrey;

    &:not(:focus-visible) {
      background: transparent;
    }

    &:focus-visible {
      outline: 2px solid red;
      background: transparent;
    }
  }
}

.frame {
  padding: 3rem var(--page-padding) 0;
  display: grid;
  z-index: 1000;
  position: relative;
  grid-row-gap: 1rem;
  grid-column-gap: 2rem;
  pointer-events: none;
  justify-items: start;
  grid-template-columns: auto auto auto 1fr;
  grid-template-areas:
    'title title title title'
    'back archive github ...'
    'demos demos demos demos'
    'tags tags tags tags'
    'sponsor sponsor sponsor sponsor';

  #cdawrap {
    justify-self: start;
    grid-area: sponsor;
  }

  a,
  button {
    pointer-events: auto;
  }

  .frame__title {
    grid-area: title;
    font-size: inherit;
    margin: 0;
  }

  .frame__back {
    grid-area: back;
    justify-self: start;
  }

  .frame__archive {
    grid-area: archive;
    justify-self: start;
  }

  .frame__github {
    grid-area: github;
  }

  .frame__tags {
    grid-area: tags;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  .frame__demos {
    grid-area: demos;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  @media screen and (min-width: 53em) {
    padding: var(--page-padding);
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    grid-template-columns: auto auto auto auto 1fr;
    grid-template-rows: auto auto;
    align-content: space-between;
    grid-template-areas:
      'title back github archive demos'
      'tags tags tags sponsor sponsor';

    .frame__tags {
      align-self: end;
    }

    .frame__demos,
    #cdawrap {
      justify-self: end;
      text-align: right;
      max-width: 300px;
    }
  }
}

.content {
  padding: var(--page-padding);
  display: flex;
  flex-direction: column;
  width: 100vw;
  position: relative;

  @media screen and (min-width: 53em) {
    min-height: 100vh;
    justify-content: center;
    align-items: center;
  }
}
src/css/style.css
파일 저장

body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  background-color: #222222;
}

.webgl {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
src/index.js
파일 저장

import './css/style.css';
import './css/base.css';
import imageUrl from './image/dithering_object.jpg';

import * as THREE from "three";
import Grid from "./Grid/Grid.js";
import { Pane } from 'tweakpane';
import * as EssentialsPlugin from '@tweakpane/plugin-essentials';

//Setup scene
const scene = new THREE.Scene();
scene.background = new THREE.Color("#6171E5");

//Setup camera
const camera = new THREE.OrthographicCamera();

camera.position.set(0, 0, 400);
camera.lookAt(0, 0, 0);
camera.near = 0.01;
camera.far = 1000;

const cameraAnchor = new THREE.Group();
cameraAnchor.name = "cameraAnchor";
cameraAnchor.add(camera);
scene.add(cameraAnchor);

//Set camera zoom and update projection matrix
camera.zoom = 0.9;
camera.updateProjectionMatrix();

//Set camera rotation
//Camera shot from above and a bit from the side 
cameraAnchor.rotation.reorder("YXZ");
cameraAnchor.rotation.y = Math.PI * 0.25;
cameraAnchor.rotation.x = -Math.PI * 0.15;

//Setup renderer
const canvas = document.querySelector("canvas.webgl");
const renderer = new THREE.WebGLRenderer({
  canvas,
  preserveDrawingBuffer: true,
  antialias: true
});


//Setup resize function
const resize = (width, height, pixelRatio) => {
  const boundingBoxSize = 400;
  const aspectRatio = width / height;

  //Resize camera
  if (aspectRatio < 1) {
    camera.left = -boundingBoxSize / 2;
    camera.right = boundingBoxSize / 2;
    camera.top = boundingBoxSize / 2 / aspectRatio;
    camera.bottom = -boundingBoxSize / 2 / aspectRatio;
  } else {
    camera.left = (-boundingBoxSize / 2) * aspectRatio;
    camera.right = (boundingBoxSize / 2) * aspectRatio;
    camera.top = boundingBoxSize / 2;
    camera.bottom = -boundingBoxSize / 2;
  }

  camera.aspect = aspectRatio;
  camera.updateProjectionMatrix();

  //Resize renderer
  renderer.setSize(width, height);
  renderer.setPixelRatio(Math.min(pixelRatio, 2));

};

//Init resize function for the 1st time
resize(window.innerWidth, window.innerHeight, window.devicePixelRatio);

//Init grid and show it on the scene
const grid = new Grid({
  name: "grid",
  rowCount: 400,
  columnCount: 400,
  cellSize: 1,
  cellThickness: 0.5,
  gridType: 1,
  image: imageUrl, // Path to the image to be used in the grid
  zPositionRange: new THREE.Vector2(20, -20),
  cellScaleRange: new THREE.Vector2(1, 1), // property to control cell scale animation
  animationMinDelay: 0, // property for minimum animation delay
  animationMaxDelay: 0.9, // property for maximum animation delay
});
grid.showAt(scene);

const thresholdMapGrid = new Grid({
  name: "thresholdMapGrid",
  rowCount: 400,
  columnCount: 400,
  cellSize: 1,
  cellThickness: 0.1,
  gridType: 2,
  zPositionRange: new THREE.Vector2(0, -20),
  cellScaleRange: new THREE.Vector2(1, 0), // property to control cell scale animation
  animationMinDelay: 0.05, // property for minimum animation delay
  animationMaxDelay: 0.95, // property for maximum animation delay
});
thresholdMapGrid.showAt(scene);



// Set animation Loop to render the scene
const animateLoop = () => {
  renderer.render(scene, camera);
  requestAnimationFrame(animateLoop);
};
animateLoop();

// Init Tweakpane
const pane = new Pane({ title: 'Settings', expanded: true });
pane.registerPlugin(EssentialsPlugin);

// Create Image Grid Settings Folder
const imageGridFolder = pane.addFolder({ title: 'Image Grid' });

const showImageGrid = imageGridFolder.addBinding({show: true}, 'show', {
  label: 'Show',
});

showImageGrid.on('change', (ev) => {
  if (ev.value) {
    grid.showAt(scene);
  } else {
    grid.hideFrom(scene);
  }
});

// Create Threshold Map Grid Settings Folder
const thresholdMapGridFolder = pane.addFolder({ title: 'Threshold Map Grid' });

const showThresholdMapGrid = thresholdMapGridFolder.addBinding({show: true}, 'show', {
  label: 'Show',
});

showThresholdMapGrid.on('change', (ev) => {
  if (ev.value) {
    thresholdMapGrid.showAt(scene);
  } else {
    thresholdMapGrid.hideFrom(scene);
  }
});


// Create Dithering Folder
const ditheringFolder = pane.addFolder({ title: 'Dithering' });

const activeThresholdMaps = {
  value: 'bayer4x4',
};

const ditheringThresholdController = ditheringFolder.addBinding(activeThresholdMaps, 'value', {
  view: 'radiogrid',
  groupName: 'ditheringThreshold',
  size: [2, 2],
  cells: (x, y) => ({
    title: `${grid.thresholdMaps[y * 2 + x].name}`,
    value: grid.thresholdMaps[y * 2 + x].id,
  }),
  label: 'Threshold Map',
})

ditheringThresholdController.on('change', (ev) => {
  grid.geometry.setAttribute("aDitheringThreshold", grid.attributes.aDitheringThresholds[ev.value]);
  thresholdMapGrid.geometry.setAttribute("aDitheringThreshold", thresholdMapGrid.attributes.aDitheringThresholds[ev.value]);
});


// Create Animation Folder
const animationFolder = pane.addFolder({ title: 'Animation' });

//Add Dropdown to select delay type
const delayTypeController = animationFolder.addBlade({
  view: 'list',
  label: 'Delay Type',
  options: {
    'Cell by Cell': 1,
    'Row by Row': 2,
    'Column by Column': 3,
    'Random': 4,
    'Corner to Corner': 5,
  },
  value: grid.material.defines.DELAY_TYPE,
});

delayTypeController.on('change', (ev) => {
  grid.material.defines.DELAY_TYPE = ev.value;
  grid.material.needsUpdate = true;

  thresholdMapGrid.material.defines.DELAY_TYPE = ev.value;
  thresholdMapGrid.material.needsUpdate = true;
});

// Add Progress Slider to control animation progress
const animationDelay = animationFolder.addBlade({
  view: 'slider',
  label: 'Max Delay',
  value: grid.material.uniforms.uAnimationMaxDelay.value,
  min: 0.05,
  max: 1,
  step: 0.01,
});
animationDelay.on('change', (ev) => {
  grid.material.uniforms.uAnimationMaxDelay.value = ev.value;

  const animationDuration = 1.0 - ev.value;
  thresholdMapGrid.material.uniforms.uAnimationMinDelay.value = animationDuration * 0.5;
  thresholdMapGrid.material.uniforms.uAnimationMaxDelay.value = ev.value + animationDuration * 0.5;
});

const progressSlider = animationFolder.addBlade({
  view: 'slider',
  label: 'Progress',
  value: 0,
  min: 0,
  max: 1,
  step: 0.01,
});
progressSlider.on('change', (ev) => {
  // Update the shader uniform with the new animation progress value
  grid.material.uniforms.uAnimationProgress.value = ev.value;
  thresholdMapGrid.material.uniforms.uAnimationProgress.value = ev.value;
});



//Reinit resize function on window resize
window.addEventListener("resize", () => {
  resize(window.innerWidth, window.innerHeight, window.devicePixelRatio);
});
vite.config.js
파일 저장

import { defineConfig } from "vite";
import glsl from "vite-plugin-glsl";

export default defineConfig({
  plugins: [glsl()],
  server: {
    port: 8080,
    open: true
  },
  build: {
    outDir: "dist",
    assetsDir: "assets"
  }
});
Original author attribution실행 안내·자료
파일 저장

Animating 160,000 Cubes in Three.js to Visualize Dithering
Original author: Damar Aji Pramudita
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
Media credits and license evidence실행 안내·자료
파일 저장

README.md
## License

[MIT](LICENSE)
Bundled dependency licenses실행 안내·자료
파일 저장

three@0.175.0 — LICENSE
The MIT License

Copyright © 2010-2025 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


@tweakpane/plugin-essentials@0.2.1 — LICENSE.txt
Copyright (c) 2021 cocopon <cocopon@me.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.