Codrops 원본
Animating 160,000 Cubes in Three.js to Visualize Dithering
섹션 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
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.
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.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
큐브마다 저장한 임계값과 지연값
화면 크기·입력·동시 작업을 고정하고 실제 렌더링 실패와 비용을 관찰합니다.
- 이 예제에서는
- Grid.init은 같은 BoxGeometry에 셀·행·열의 정규화 값과 디더링 임계값을 InstancedBufferAttribute로 넣습니다. 큐브별 행렬은 배치 때 만들고 진행값은 재질 uniform으로 전달합니다.
