I’m trying to create a reverse halftone effect using Threlte. This is my reference image:
There are a couple of layers of dots, with some rather large areas of transparency, and colored layers that are offset, with an Additive Blend Mode. I’ve built out a component that can generate the dots, the colors, and the offset in a really nice way, but the problem that I’m running into is that the dots themselves are what is revealing the color, rather than the material behind the dots being the thing that’s revealing the color:
I’m working with a designer on this, and I have a textural image I could utilize if necessary, although the quality dips when I do (texture is below).
Overall, I like the placement of the dots in my component using simplexNoise, the color is something I still need to tweak to match exactly, but overall I am looking to adjust my component to draw the dots, put the colored plane behind them, and then reveal the colors from that portion of what’s being drawn in the scene. Below is the current iteration of my component:
<script lang="ts">
import { T, Canvas } from '@threlte/core';
import type { InstancedMesh, PerspectiveCamera } from 'three';
import { onMount } from 'svelte';
import * as THREE from 'three';
import { createNoise2D } from 'simplex-noise';
const gridSize = 120;
const dotSpacing = 0.25;
const maxDotSize = 0.12;
const minDotSize = 0;
const stackCount = 3;
const yOffset = 0.24;
const stackColors = ['rgb(0,0,255)', 'rgb(0,255,0)', 'rgb(255,0,0)'];
const instanceCount = gridSize * gridSize * stackCount;
const simplex = createNoise2D();
const positions: Array<[number, number, number]> = [];
const scales: number[] = [];
const colors: THREE.Color[] = [];
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
const xNorm = x / gridSize;
const yNorm = y / gridSize;
// Use simplex noise for clustering
const noise = simplex(xNorm * 4, yNorm * 4);
const normalized = (noise + 1) / 2;
const exponent = 3;
const dotSize = minDotSize + (maxDotSize - minDotSize) * Math.pow(normalized, exponent);
const xPos = (x - gridSize / 2) * dotSpacing;
const yPos = (y - gridSize / 2) * dotSpacing;
for (let i = 0; i < stackCount; i++) {
positions.push([xPos, yPos + i * yOffset, 0]);
scales.push(dotSize);
colors.push(new THREE.Color(stackColors[i % stackColors.length]));
}
}
}
let camera: PerspectiveCamera;
let instancedMesh: InstancedMesh;
onMount(() => {
if (!instancedMesh) return;
const dummy = new THREE.Matrix4();
for (let i = 0; i < instanceCount; i++) {
const [x, y, z] = positions[i];
const scale = scales[i];
dummy.makeTranslation(x, y, z);
dummy.scale(new THREE.Vector3(scale, scale, 1));
instancedMesh.setMatrixAt(i, dummy);
instancedMesh.setColorAt(i, colors[i]);
}
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.instanceColor!.needsUpdate = true;
});
</script>
<T.PerspectiveCamera bind:ref={camera} makeDefault position={[0, 0, 10]} fov={50} />
<T.InstancedMesh args={[null, null, instanceCount]} bind:ref={instancedMesh} frustumCulled={false}>
<T.CircleGeometry args={[1, 25]} />
<T.MeshBasicMaterial blending={THREE.AdditiveBlending} toneMapped={false} />
</T.InstancedMesh>
If you have an answer, it doesn’t need to be Svelte/Threlte specific, I can port it from any framework, or vanilla Three.js as necessary. Thanks in advance!