diff --git a/src/entities/SufferTexts.ts b/src/entities/SufferTexts.ts new file mode 100644 index 0000000..ff1708c --- /dev/null +++ b/src/entities/SufferTexts.ts @@ -0,0 +1,144 @@ +import { Entity } from '../heck/Entity'; +import { GPUParticles } from './GPUParticles'; +import { Geometry } from '../heck/Geometry'; +import { InstancedGeometry } from '../heck/InstancedGeometry'; +import { Material, MaterialMap } from '../heck/Material'; +import quadVert from '../shaders/quad.vert'; +import sufferTextsComputeFrag from '../shaders/suffer-texts-compute.frag'; +import sufferTextsRenderFrag from '../shaders/suffer-texts-render.frag'; +import sufferTextsRenderVert from '../shaders/suffer-texts-render.vert'; +import { TRIANGLE_STRIP_QUAD } from '@fms-cat/experimental'; +import { gl, glCat } from '../globals/canvas'; +import { randomTextureStatic } from '../globals/randomTexture'; +import { tinyCharTexture } from '../globals/tinyCharTexture'; +import { Lambda } from '../heck/components/Lambda'; +import { auto } from '../globals/automaton'; +import { sufferList } from '../sufferList'; + +const PARTICLES = 256; + +export class SufferTexts { + public get entity(): Entity { + return this.gpuParticles.entity; + } + + public gpuParticles: GPUParticles; + public queue: number[][]; + public particleIndex: number; + + public constructor() { + this.gpuParticles = new GPUParticles( { + materialCompute: this.__createMaterialCompute(), + geometryRender: this.__createGeometryRender(), + materialsRender: this.__createMaterialsRender(), + computeWidth: PARTICLES, + computeHeight: 1, + computeNumBuffers: 1, + namePrefix: process.env.DEV && 'SufferTexts', + } ); + + this.queue = []; + + this.particleIndex = 0; + + auto( 'sufferText/push', ( { value } ) => { + const suffer = sufferList[ Math.floor( value ) ].split( '\n' ); + suffer.forEach( ( line, iLine ) => { + const chars = [ ...line ]; + this.queue.push( ...chars.map( ( char, iChar ) => [ + iChar - 0.5 * ( chars.length - 1 ), + iLine - 0.5 * ( suffer.length - 1 ), + char.charCodeAt( 0 ), + ] ) ); + } ); + } ); + + this.entity.components.push( new Lambda( { + onUpdate: () => { + const val = this.queue.shift(); + if ( val != null ) { + const x = ( this.particleIndex + 0.5 ) / PARTICLES; + this.gpuParticles.materialCompute.addUniform( 'logInit', '4f', ...val, x ); + this.particleIndex = ( this.particleIndex + 1 ) % PARTICLES; + } else { + this.gpuParticles.materialCompute.addUniform( 'logInit', '4f', 0.0, 0.0, 0.0, 0.0 ); + } + }, + } ) ); + } + + private __createMaterialCompute(): Material { + const material = new Material( quadVert, sufferTextsComputeFrag ); + if ( process.env.DEV ) { + if ( module.hot ) { + module.hot.accept( '../shaders/suffer-texts-compute.frag', () => { + material.replaceShader( quadVert, sufferTextsComputeFrag ); + } ); + } + } + + return material; + } + + private __createGeometryRender(): Geometry { + const geometry = new InstancedGeometry(); + + const bufferP = glCat.createBuffer(); + bufferP.setVertexbuffer( new Float32Array( TRIANGLE_STRIP_QUAD ) ); + + geometry.addAttribute( 'position', { + buffer: bufferP, + size: 2, + type: gl.FLOAT, + } ); + + const bufferComputeUV = glCat.createBuffer(); + bufferComputeUV.setVertexbuffer( ( () => { + const ret = new Float32Array( PARTICLES ); + for ( let ix = 0; ix < PARTICLES; ix ++ ) { + const s = ( ix + 0.5 ) / PARTICLES; + ret[ ix ] = s; + } + return ret; + } )() ); + + geometry.addAttribute( 'computeX', { + buffer: bufferComputeUV, + size: 1, + divisor: 1, + type: gl.FLOAT + } ); + + geometry.count = 4; + geometry.mode = gl.TRIANGLE_STRIP; + geometry.primcount = PARTICLES; + + return geometry; + } + + private __createMaterialsRender(): MaterialMap<'deferred'> { + const deferred = new Material( + sufferTextsRenderVert, + sufferTextsRenderFrag, + { defines: { 'DEFERRED': 'true' } }, + ); + deferred.addUniformTexture( 'samplerRandomStatic', randomTextureStatic.texture ); + deferred.addUniformTexture( 'samplerTinyChar', tinyCharTexture ); + + if ( process.env.DEV ) { + if ( module.hot ) { + module.hot.accept( + [ + '../shaders/suffer-texts-render.vert', + '../shaders/suffer-texts-render.frag', + ], + () => { + deferred.replaceShader( sufferTextsRenderVert, sufferTextsRenderFrag ); + } + ); + } + } + + return { deferred }; + } +} diff --git a/src/globals/tinyCharTexture.ts b/src/globals/tinyCharTexture.ts new file mode 100644 index 0000000..de829dc --- /dev/null +++ b/src/globals/tinyCharTexture.ts @@ -0,0 +1,11 @@ +import { gl, glCat } from './canvas'; +import char5x5Png from '../images/char5x5.png'; + +export const tinyCharTexture = glCat.createTexture(); + +const image = new Image(); +image.onload = () => { + tinyCharTexture.setTexture( image ); + tinyCharTexture.textureFilter( gl.NEAREST ); +}; +image.src = char5x5Png; diff --git a/src/images/char5x5.png b/src/images/char5x5.png new file mode 100644 index 0000000..ae9ba00 Binary files /dev/null and b/src/images/char5x5.png differ diff --git a/src/png.d.ts b/src/png.d.ts new file mode 100644 index 0000000..6b6b322 --- /dev/null +++ b/src/png.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const uri: string; + export default uri; +} diff --git a/src/scene.ts b/src/scene.ts index 79a9e38..6bb5aae 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -14,6 +14,7 @@ import { Raymarcher } from './entities/Raymarcher'; import { Rings } from './entities/Rings'; import { RTInspector } from './entities/RTInspector'; import { SphereParticles } from './entities/SphereParticles'; +import { SufferTexts } from './entities/SufferTexts'; import { Trails } from './entities/Trails'; import { auto, automaton } from './globals/automaton'; import { music } from './globals/music'; @@ -133,6 +134,16 @@ if ( process.env.DEV && module.hot ) { } ); } +const replacerSufferTexts = new EntityReplacer( + () => new SufferTexts(), + 'SufferTexts', +); +if ( process.env.DEV && module.hot ) { + module.hot.accept( './entities/SufferTexts', () => { + replacerSufferTexts.replace(); + } ); +} + const replacerRaymarcher = new EntityReplacer( () => new Raymarcher(), 'Raymarcher' ); if ( process.env.DEV && module.hot ) { module.hot.accept( './entities/Raymarcher', () => { diff --git a/src/shaders/suffer-texts-compute.frag b/src/shaders/suffer-texts-compute.frag new file mode 100644 index 0000000..1a1f285 --- /dev/null +++ b/src/shaders/suffer-texts-compute.frag @@ -0,0 +1,24 @@ +#version 300 es + +precision highp float; + +out vec4 fragCompute0; + +uniform float time; +uniform float deltaTime; +uniform vec2 resolution; +uniform vec4 logInit; +uniform sampler2D samplerCompute0; + +void main() { + vec2 uv = gl_FragCoord.xy / resolution; + + fragCompute0 = texture( samplerCompute0, uv ); + + if ( logInit.w == uv.x ) { + fragCompute0 = logInit; + fragCompute0.w = 0.0; // life + } + + fragCompute0.w += deltaTime; // life +} diff --git a/src/shaders/suffer-texts-render.frag b/src/shaders/suffer-texts-render.frag new file mode 100644 index 0000000..c94acb3 --- /dev/null +++ b/src/shaders/suffer-texts-render.frag @@ -0,0 +1,40 @@ +#version 300 es + +precision highp float; + +const int MTL_UNLIT = 1; + +// == varings / uniforms =========================================================================== +in float vLife; +in float vMode; +in vec2 vUv; +in vec2 vSize; +in vec3 vNormal; +in vec4 vPosition; +in vec4 vDice; + +uniform float time; +uniform sampler2D samplerRandomStatic; +uniform sampler2D samplerTinyChar; + +#ifdef DEFERRED + layout (location = 0) out vec4 fragPosition; + layout (location = 1) out vec4 fragNormal; + layout (location = 2) out vec4 fragColor; + layout (location = 3) out vec4 fragWTF; +#endif + +// == main procedure =============================================================================== +void main() { + if ( vLife > 1.0 ) { discard; } + + float tex = texture( samplerTinyChar, vUv ).x; + if ( tex < 0.5 ) { discard; } + + #ifdef DEFERRED + fragPosition = vPosition; + fragNormal = vec4( vNormal, 1.0 ); + fragColor = vec4( 1.0 ); + fragWTF = vec4( vec3( 0.0 ), MTL_UNLIT ); + #endif +} diff --git a/src/shaders/suffer-texts-render.vert b/src/shaders/suffer-texts-render.vert new file mode 100644 index 0000000..4a13f45 --- /dev/null +++ b/src/shaders/suffer-texts-render.vert @@ -0,0 +1,66 @@ +#version 300 es + +#define fs(i) (fract(sin((i)*114.514)*1919.810)) +#define saturate(i) clamp(i,0.,1.) +#define lofi(i,m) (floor((i)/(m))*(m)) +#define lofir(i,m) (floor((i+0.5)/(m))*(m)) + +// ------------------------------------------------------------------------------------------------- + +in float computeX; +in vec2 position; + +out float vLife; +out vec2 vUv; +out vec2 vSize; +out vec3 vNormal; +out vec4 vPosition; + +uniform vec2 resolution; +uniform vec2 resolutionCompute; +uniform mat4 projectionMatrix; +uniform mat4 viewMatrix; +uniform mat4 modelMatrix; +uniform mat4 normalMatrix; +uniform sampler2D samplerCompute0; +uniform sampler2D samplerRandomStatic; + +// == utils ======================================================================================== +vec2 yflip( vec2 uv ) { + return vec2( 0.0, 1.0 ) + vec2( 1.0, -1.0 ) * uv; +} + +// == main procedure =============================================================================== +void main() { + vec2 computeUV = vec2( computeX, 0.5 ); + vec4 tex0 = texture( samplerCompute0, computeUV ); + + // == assign varying variables =================================================================== + vLife = tex0.w; + + float char = tex0.z; + char += 64.0 * ( min( vLife, 0.3 ) + max( vLife, 0.7 ) - 1.0 ); + + vUv = yflip( 0.5 + 0.499 * position ); + vUv = ( vUv + floor( mod( vec2( char / vec2( 1.0, 16.0 ) ), 16.0 ) ) ) / 16.0; + + vNormal = normalize( ( normalMatrix * vec4( 0.0, 0.0, 1.0, 1.0 ) ).xyz ); + + // == compute size =============================================================================== + float scale = 0.0625; + + vPosition = vec4( scale * 1.2 * tex0.xy, 0.0, 1.0 ); + vPosition.y = -vPosition.y; + + vec2 shape = position * scale * 0.5; + + vPosition.xy += shape; + + // == send the vertex position =================================================================== + vPosition = vPosition; + vec4 outPos = vPosition; + outPos.x *= resolution.y / resolution.x; + gl_Position = outPos; + + vPosition.w = outPos.z / outPos.w; +} diff --git a/src/sufferList.ts b/src/sufferList.ts new file mode 100644 index 0000000..d3b49c0 --- /dev/null +++ b/src/sufferList.ts @@ -0,0 +1,18 @@ +export const sufferList = [ + '#DEFINE DISGRACE 1', + 'CTRL + ALT + DESPAIR', + 'PUBIC CONSTRUCTOR()', + 'LIBOPUS IS CHEATING', + 'PUBLIC GET FUCKED()', + '\'RETRUN\': UNDECLARED IDENTIFIER', + 'NOTICE ME, GARBAGE COLLECTOR', + 'WEBGL HATES YOU', + '#DEFINE COMPROMISE 1', + 'GL.DISABLE(GL.TIMEZONE)', + 'WHERE IS MY SLEEPING SCHEDULE?', + 'SVG.GETPOINTATLENGTH IS CHEATING', + 'COPY\'N\'PASTE ENGINEER', + 'ENGLISH SUCKS', + '60FPS OR DIE', + 'END MY SUFFER', +]; diff --git a/webpack.config.js b/webpack.config.js index 7ee17fe..c9cf26f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -53,7 +53,7 @@ module.exports = ( env, argv ) => { ], }, { - test: /\.opus$/, + test: /\.(opus|png)$/, type: 'asset/inline', }, {