From 5ce339fcec0573443fe1f2c68a09989c34b53e92 Mon Sep 17 00:00:00 2001 From: LucaArgentieri Date: Sat, 15 Feb 2025 14:09:07 +0100 Subject: [PATCH 1/2] created AsciiEffect (to improve), Ascii Content Page and Ascii Demo (to fix) --- demo/src/demos/AsciiDemo.js | 186 +++++++++++++++++ demo/src/index.js | 20 +- manual/assets/js/src/demos/ascii.js | 137 ++++++++++++ .../content/demos/special-effects/ascii.en.md | 15 ++ src/effects/AsciiEffect.js | 195 ++++++++++++++++++ src/effects/glsl/ascii.frag | 43 ++++ src/effects/index.js | 1 + 7 files changed, 588 insertions(+), 9 deletions(-) create mode 100644 demo/src/demos/AsciiDemo.js create mode 100644 manual/assets/js/src/demos/ascii.js create mode 100644 manual/content/demos/special-effects/ascii.en.md create mode 100644 src/effects/AsciiEffect.js create mode 100644 src/effects/glsl/ascii.frag diff --git a/demo/src/demos/AsciiDemo.js b/demo/src/demos/AsciiDemo.js new file mode 100644 index 000000000..07ab18925 --- /dev/null +++ b/demo/src/demos/AsciiDemo.js @@ -0,0 +1,186 @@ +import { Color, PerspectiveCamera } from "three"; +import { SpatialControls } from "spatial-controls"; +import { calculateVerticalFoV } from "three-demo"; +import { ProgressManager } from "../utils/ProgressManager"; +import { PostProcessingDemo } from "./PostProcessingDemo"; + +import * as Sponza from "./objects/Sponza"; + +import { + EdgeDetectionMode, + EffectPass, + AsciiEffect, + SMAAEffect, + SMAAImageLoader, + SMAAPreset +} from "../../../src"; + +/** + * Ascii demo. + */ + +export class AsciiDemo extends PostProcessingDemo { + + /** + * Constructs a new ascii demo. + * + * @param {EffectComposer} composer - An effect composer. + */ + + constructor(composer) { + + super("ascii", composer); + + /** + * An effect. + * + * @type {Effect} + * @private + */ + + this.effect = null; + + } + + load() { + + const assets = this.assets; + const loadingManager = this.loadingManager; + const smaaImageLoader = new SMAAImageLoader(loadingManager); + + const anisotropy = Math.min(this.composer.getRenderer() + .capabilities.getMaxAnisotropy(), 8); + + return new Promise((resolve, reject) => { + + if (assets.size === 0) { + + loadingManager.onLoad = () => setTimeout(resolve, 250); + loadingManager.onProgress = ProgressManager.updateProgress; + loadingManager.onError = url => console.error(`Failed to load ${url}`); + + Sponza.load(assets, loadingManager, anisotropy); + + smaaImageLoader.load(([search, area]) => { + + assets.set("smaa-search", search); + assets.set("smaa-area", area); + + }); + + } else { + + resolve(); + + } + + }); + + } + + initialize() { + + const scene = this.scene; + const assets = this.assets; + const composer = this.composer; + const renderer = composer.getRenderer(); + const domElement = renderer.domElement; + + // Camera + + const aspect = window.innerWidth / window.innerHeight; + const vFoV = calculateVerticalFoV(90, Math.max(aspect, 16 / 9)); + const camera = new PerspectiveCamera(vFoV, aspect, 0.3, 2000); + this.camera = camera; + + // Controls + + const { position, quaternion } = camera; + const controls = new SpatialControls(position, quaternion, domElement); + const settings = controls.settings; + settings.rotation.sensitivity = 2.2; + settings.rotation.damping = 0.05; + settings.translation.sensitivity = 3.0; + settings.translation.damping = 0.1; + controls.position.set(-9, 0.5, 0); + controls.lookAt(0, 3, -3.5); + this.controls = controls; + + // Sky + + scene.background = new Color(0xeeeeee); + + // Lights + + scene.add(...Sponza.createLights()); + + // Objects + + scene.add(assets.get(Sponza.tag)); + + // Passes + + const smaaEffect = new SMAAEffect( + assets.get("smaa-search"), + assets.get("smaa-area"), + SMAAPreset.HIGH, + EdgeDetectionMode.DEPTH + ); + + smaaEffect.edgeDetectionMaterial.setEdgeDetectionThreshold(0.01); + + const asciiEffect = new AsciiEffect({ + fontSize: 35, + cellSize: 16, + invert: false, + color: "#ffffff", + characters: ` .:,'-^=*+?!|0#X%WM@` + }); + + const effectPass = new EffectPass(camera, asciiEffect); + // const smaaPass = new EffectPass(camera, smaaEffect); + + this.effect = asciiEffect; + + // composer.addPass(smaaPass); + composer.addPass(effectPass); + + } + + registerOptions(menu) { + + const effect = this.effect; + + const params = { + font: "arial", + characters: ` .:,'-^=*+?!|0#X%WM@`, + fontSize: 54, + cellSize: 16, + color: "#ffffff", + invert: false + }; + + // menu.add(params, "fontSize", 0, 50, 1).onChange((value) => { + // effect.fontSize = value; + // }); + + // menu.add(params, "cellSize", 1, 32, 1).onChange((value) => { + // effect.cellSize = value; + // }); + + // menu.addColor(params, "color").onChange((value) => { + // effect.color = value; + // }); + + // menu.add(params, "invert").onChange((value) => { + // effect.invert = value; + // }); + + if (window.innerWidth < 720) { + menu.close(); + + } + + } + +} diff --git a/demo/src/index.js b/demo/src/index.js index aa847766d..b1546c0f5 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -11,6 +11,7 @@ import { EffectComposer, OverrideMaterialManager } from "../../src"; import { ProgressManager } from "./utils/ProgressManager"; import { AntialiasingDemo } from "./demos/AntialiasingDemo"; +import { AsciiDemo } from "./demos/AsciiDemo"; import { BloomDemo } from "./demos/BloomDemo"; import { BlurDemo } from "./demos/BlurDemo"; import { ColorDepthDemo } from "./demos/ColorDepthDemo"; @@ -83,7 +84,7 @@ window.addEventListener("load", (event) => { const camera = demo.getCamera(); demo.renderPass.camera = camera; - if(!demoCache.has(demo)) { + if (!demoCache.has(demo)) { // Prevent stuttering when new objects come into view. demo.scene.traverse((node) => void (node.frustumCulled = false)); @@ -103,11 +104,11 @@ window.addEventListener("load", (event) => { const height = window.innerHeight; const demo = manager.getCurrentDemo(); - if(demo !== null) { + if (demo !== null) { const camera = demo.getCamera(); - if(camera !== null) { + if (camera !== null) { const aspect = Math.max(width / height, 16 / 9); const vFoV = calculateVerticalFoV(90, aspect); @@ -124,7 +125,7 @@ window.addEventListener("load", (event) => { document.addEventListener("keyup", (event) => { - if(event.key === "h") { + if (event.key === "h") { const aside = document.querySelector("aside"); const footer = document.querySelector("footer"); @@ -133,11 +134,11 @@ window.addEventListener("load", (event) => { aside.classList.toggle("hidden"); footer.classList.toggle("hidden"); - } else if(event.key === "c") { + } else if (event.key === "c") { const camera = manager.getCurrentDemo().getCamera(); - if(camera !== null) { + if (camera !== null) { const v = new Vector3(); console.log("Camera position", camera.position); @@ -155,6 +156,7 @@ window.addEventListener("load", (event) => { const demos = [ new AntialiasingDemo(composer), + new AsciiDemo(composer), new BloomDemo(composer), new BlurDemo(composer), new ColorDepthDemo(composer), @@ -175,14 +177,14 @@ window.addEventListener("load", (event) => { const id = window.location.hash.slice(1); const exists = demos.reduce((a, b) => (a || b.id === id), false); - if(!exists) { + if (!exists) { // Invalid URL hash: demo doesn't exist. window.location.hash = ""; } - for(const demo of demos) { + for (const demo of demos) { manager.addDemo(demo); @@ -203,7 +205,7 @@ document.addEventListener("DOMContentLoaded", (event) => { const img = document.querySelector(".info img"); const div = document.querySelector(".info div"); - if(img !== null && div !== null) { + if (img !== null && div !== null) { img.addEventListener("click", (event) => { diff --git a/manual/assets/js/src/demos/ascii.js b/manual/assets/js/src/demos/ascii.js new file mode 100644 index 000000000..9e37e14e9 --- /dev/null +++ b/manual/assets/js/src/demos/ascii.js @@ -0,0 +1,137 @@ +import { + CubeTextureLoader, + FogExp2, + LoadingManager, + PerspectiveCamera, + Scene, + SRGBColorSpace, + WebGLRenderer +} from "three"; + +import { + AsciiEffect, + EffectComposer, + EffectPass, + RenderPass +} from "postprocessing"; + +import { Pane } from "tweakpane"; +import { SpatialControls } from "spatial-controls"; +import { calculateVerticalFoV, FPSMeter } from "../utils"; +import * as Domain from "../objects/Domain"; + +function load() { + + const assets = new Map(); + const loadingManager = new LoadingManager(); + const cubeTextureLoader = new CubeTextureLoader(loadingManager); + + const path = document.baseURI + "img/textures/skies/sunset/"; + const format = ".png"; + const urls = [ + path + "px" + format, path + "nx" + format, + path + "py" + format, path + "ny" + format, + path + "pz" + format, path + "nz" + format + ]; + + return new Promise((resolve, reject) => { + + loadingManager.onLoad = () => resolve(assets); + loadingManager.onError = (url) => reject(new Error(`Failed to load ${url}`)); + + cubeTextureLoader.load(urls, (t) => { + + t.colorSpace = SRGBColorSpace; + assets.set("sky", t); + + }); + + }); + +} + +window.addEventListener("load", () => load().then((assets) => { + + // Renderer + + const renderer = new WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: false + }); + + renderer.debug.checkShaderErrors = (window.location.hostname === "localhost"); + const container = document.querySelector(".viewport"); + container.prepend(renderer.domElement); + + // Camera & Controls + + const camera = new PerspectiveCamera(); + const controls = new SpatialControls(camera.position, camera.quaternion, renderer.domElement); + const settings = controls.settings; + settings.rotation.sensitivity = 2.2; + settings.rotation.damping = 0.05; + settings.translation.damping = 0.1; + controls.position.set(0, 0, 1); + controls.lookAt(0, 0, 0); + + // Scene, Lights, Objects + + const scene = new Scene(); + scene.fog = new FogExp2(0x373134, 0.06); + scene.background = assets.get("sky"); + scene.add(Domain.createLights()); + + // Post Processing + + const composer = new EffectComposer(renderer); + + const effect = new AsciiEffect({ + font: "arial", + characters: ` .:,'-^=*+?!|0#X%WM@`, + fontSize: 35, + cellSize: 16, + color: "#ffffff", + invert: false + }); + composer.addPass(new RenderPass(scene, camera)); + composer.addPass(new EffectPass(camera, effect)); + + // Settings + + const fpsMeter = new FPSMeter(); + const pane = new Pane({ container: container.querySelector(".tp") }); + pane.addBinding(fpsMeter, "fps", { readonly: true, label: "FPS" }); + + const folder = pane.addFolder({ title: "Settings" }); + // folder.addBinding(effect, "granularity", { min: 0, max: 20, step: 1 }); + + + // Resize Handler + + function onResize() { + + const width = container.clientWidth, height = container.clientHeight; + camera.aspect = width / height; + camera.fov = calculateVerticalFoV(90, Math.max(camera.aspect, 16 / 9)); + camera.updateProjectionMatrix(); + composer.setSize(width, height); + + } + + window.addEventListener("resize", onResize); + onResize(); + + // Render Loop + + requestAnimationFrame(function render(timestamp) { + + fpsMeter.update(timestamp); + controls.update(timestamp); + composer.render(); + requestAnimationFrame(render); + + }); + +})); diff --git a/manual/content/demos/special-effects/ascii.en.md b/manual/content/demos/special-effects/ascii.en.md new file mode 100644 index 000000000..5755d809a --- /dev/null +++ b/manual/content/demos/special-effects/ascii.en.md @@ -0,0 +1,15 @@ +--- +layout: single +collection: sections +title: ASCII +draft: false +menu: + demos: + parent: special-effects + weight: 9 +script: ascii +--- + +# ASCII + +### External Resources diff --git a/src/effects/AsciiEffect.js b/src/effects/AsciiEffect.js new file mode 100644 index 000000000..08c064dc4 --- /dev/null +++ b/src/effects/AsciiEffect.js @@ -0,0 +1,195 @@ +import { + Uniform, Vector2, Vector4, + CanvasTexture, + Color, + NearestFilter, + RepeatWrapping, + Texture, +} from "three" +import { Effect } from "./Effect.js"; + +import fragmentShader from "./glsl/ascii.frag"; + + +// export class AsciiEffect extends Effect { + +// /** +// * Constructs a new pixelation effect. +// * +// * @param {Object} [granularity=30.0] - The pixel granularity. +// */ + +// constructor(granularity = 30.0) { + +// super("AsciiEffect", fragmentShader, { +// uniforms: new Map([ +// ["active", new Uniform(false)], +// ["d", new Uniform(new Vector4())] +// ]) +// }); + +// /** +// * The original resolution. +// * +// * @type {Vector2} +// * @private +// */ + +// this.resolution = new Vector2(); + +// /** +// * Backing data for {@link granularity}. +// * +// * @type {Number} +// * @private +// */ + +// this._granularity = 0; +// this.granularity = granularity; + +// } + +// /** +// * The pixel granularity. +// * +// * A higher value yields coarser visuals. +// * +// * @type {Number} +// */ + +// get granularity() { + +// return this._granularity; + +// } + +// set granularity(value) { + +// let d = Math.floor(value); + +// if (d % 2 > 0) { + +// d += 1; + +// } + +// this._granularity = d; +// this.uniforms.get("active").value = (d > 0); +// this.setSize(this.resolution.width, this.resolution.height); + +// } + +// /** +// * Returns the pixel granularity. +// * +// * @deprecated Use granularity instead. +// * @return {Number} The granularity. +// */ + +// getGranularity() { + +// return this.granularity; + +// } + +// /** +// * Sets the pixel granularity. +// * +// * @deprecated Use granularity instead. +// * @param {Number} value - The new granularity. +// */ + +// setGranularity(value) { + +// this.granularity = value; + +// } + +// /** +// * Updates the granularity. +// * +// * @param {Number} width - The width. +// * @param {Number} height - The height. +// */ + +// setSize(width, height) { + +// const resolution = this.resolution; +// resolution.set(width, height); + +// const d = this.granularity; +// const x = d / resolution.x; +// const y = d / resolution.y; +// this.uniforms.get("d").value.set(x, y, 1.0 / x, 1.0 / y); + +// } + +// } + + + +/** + * ASCII effect. + * + * Warning: This effect cannot be merged with convolution effects. + */ + +export class AsciiEffect extends Effect { + constructor({ + font = "arial", + characters = ` .:,'-^=*+?!|0#X%WM@`, + fontSize = 54, + cellSize = 16, + color = "#ffffff", + invert = false + } = {}) { + const uniforms = new Map([ + ["uCharacters", new Uniform(new Texture())], + ["uCellSize", new Uniform(cellSize)], + ["uCharactersCount", new Uniform(characters.length)], + ["uColor", new Uniform(new Color(color))], + ["uInvert", new Uniform(invert)] + ]) + + super("ASCII", fragmentShader, { uniforms }) + + const charactersTextureUniform = this.uniforms.get("uCharacters") + + if (charactersTextureUniform) charactersTextureUniform.value = this.createCharactersTexture(characters, font, fontSize) + } + + /** Draws the characters on a Canvas and returns a texture */ + createCharactersTexture(characters, font, fontSize) { + const canvas = document.createElement("canvas") + const SIZE = 1024 + const MAX_PER_ROW = 16 + const CELL = SIZE / MAX_PER_ROW + + canvas.width = canvas.height = SIZE + const texture = new CanvasTexture( + canvas, + undefined, + RepeatWrapping, + RepeatWrapping, + NearestFilter, + NearestFilter + ) + const context = canvas.getContext("2d") + + context.clearRect(0, 0, SIZE, SIZE) + context.font = `${fontSize}px ${font}` + context.textAlign = "center" + context.textBaseline = "middle" + context.fillStyle = "#fff" + + for (let i = 0; i < characters.length; i++) { + const char = characters[i] + const x = i % MAX_PER_ROW + const y = Math.floor(i / MAX_PER_ROW) + context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2) + } + + texture.needsUpdate = true + return texture + } +} diff --git a/src/effects/glsl/ascii.frag b/src/effects/glsl/ascii.frag new file mode 100644 index 000000000..c436476ab --- /dev/null +++ b/src/effects/glsl/ascii.frag @@ -0,0 +1,43 @@ +uniform sampler2D uCharacters; +uniform float uCharactersCount; +uniform float uCellSize; +uniform bool uInvert; +uniform vec3 uColor; + +const vec2 SIZE = vec2(16.); + +vec3 greyscale(vec3 color, float strength) { + float g = dot(color, vec3(0.299, 0.587, 0.114)); + return mix(color, vec3(g), strength); +} + +vec3 greyscale(vec3 color) { + return greyscale(color, 1.0); +} + +void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { + vec2 cell = resolution / uCellSize; + vec2 grid = 1.0 / cell; + vec2 pixelizedUV = grid * (0.5 + floor(uv / grid)); + vec4 pixelized = texture2D(inputBuffer, pixelizedUV); + float greyscaled = greyscale(pixelized.rgb).r; + + if (uInvert) { + greyscaled = 1.0 - greyscaled; + } + + float characterIndex = floor((uCharactersCount - 1.0) * greyscaled); + vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y)); + vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE; + vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset; + vec4 asciiCharacter = texture2D(uCharacters, charUV); + + asciiCharacter.rgb = uColor * asciiCharacter.r; + asciiCharacter.a = pixelized.a; + + if(asciiCharacter.r == 0.0 && asciiCharacter.g == 0.0 && asciiCharacter.b == 0.0) { + asciiCharacter = vec4(0.0, 0.0, 0.0, 0.0); + } + + outputColor = asciiCharacter; +} \ No newline at end of file diff --git a/src/effects/index.js b/src/effects/index.js index b0807bcde..c53d865ad 100644 --- a/src/effects/index.js +++ b/src/effects/index.js @@ -1,5 +1,6 @@ export * from "./blending/index.js"; +export * from "./AsciiEffect.js"; export * from "./BloomEffect.js"; export * from "./BokehEffect.js"; export * from "./BrightnessContrastEffect.js"; From 130d02915653b3f56538b816a2d03e5298e2e8ec Mon Sep 17 00:00:00 2001 From: LucaArgentieri Date: Sat, 15 Feb 2025 14:36:57 +0100 Subject: [PATCH 2/2] improved ascii effect with scene color --- manual/assets/js/src/demos/ascii.js | 3 +- src/effects/AsciiEffect.js | 127 ++-------------------------- src/effects/glsl/ascii.frag | 3 +- 3 files changed, 12 insertions(+), 121 deletions(-) diff --git a/manual/assets/js/src/demos/ascii.js b/manual/assets/js/src/demos/ascii.js index 9e37e14e9..b0e626cf5 100644 --- a/manual/assets/js/src/demos/ascii.js +++ b/manual/assets/js/src/demos/ascii.js @@ -93,7 +93,8 @@ window.addEventListener("load", () => load().then((assets) => { fontSize: 35, cellSize: 16, color: "#ffffff", - invert: false + invert: false, + sceneColor: true }); composer.addPass(new RenderPass(scene, camera)); composer.addPass(new EffectPass(camera, effect)); diff --git a/src/effects/AsciiEffect.js b/src/effects/AsciiEffect.js index 08c064dc4..59825e768 100644 --- a/src/effects/AsciiEffect.js +++ b/src/effects/AsciiEffect.js @@ -11,123 +11,6 @@ import { Effect } from "./Effect.js"; import fragmentShader from "./glsl/ascii.frag"; -// export class AsciiEffect extends Effect { - -// /** -// * Constructs a new pixelation effect. -// * -// * @param {Object} [granularity=30.0] - The pixel granularity. -// */ - -// constructor(granularity = 30.0) { - -// super("AsciiEffect", fragmentShader, { -// uniforms: new Map([ -// ["active", new Uniform(false)], -// ["d", new Uniform(new Vector4())] -// ]) -// }); - -// /** -// * The original resolution. -// * -// * @type {Vector2} -// * @private -// */ - -// this.resolution = new Vector2(); - -// /** -// * Backing data for {@link granularity}. -// * -// * @type {Number} -// * @private -// */ - -// this._granularity = 0; -// this.granularity = granularity; - -// } - -// /** -// * The pixel granularity. -// * -// * A higher value yields coarser visuals. -// * -// * @type {Number} -// */ - -// get granularity() { - -// return this._granularity; - -// } - -// set granularity(value) { - -// let d = Math.floor(value); - -// if (d % 2 > 0) { - -// d += 1; - -// } - -// this._granularity = d; -// this.uniforms.get("active").value = (d > 0); -// this.setSize(this.resolution.width, this.resolution.height); - -// } - -// /** -// * Returns the pixel granularity. -// * -// * @deprecated Use granularity instead. -// * @return {Number} The granularity. -// */ - -// getGranularity() { - -// return this.granularity; - -// } - -// /** -// * Sets the pixel granularity. -// * -// * @deprecated Use granularity instead. -// * @param {Number} value - The new granularity. -// */ - -// setGranularity(value) { - -// this.granularity = value; - -// } - -// /** -// * Updates the granularity. -// * -// * @param {Number} width - The width. -// * @param {Number} height - The height. -// */ - -// setSize(width, height) { - -// const resolution = this.resolution; -// resolution.set(width, height); - -// const d = this.granularity; -// const x = d / resolution.x; -// const y = d / resolution.y; -// this.uniforms.get("d").value.set(x, y, 1.0 / x, 1.0 / y); - -// } - -// } - - - /** * ASCII effect. * @@ -141,14 +24,16 @@ export class AsciiEffect extends Effect { fontSize = 54, cellSize = 16, color = "#ffffff", - invert = false + invert = false, + sceneColor = false } = {}) { const uniforms = new Map([ ["uCharacters", new Uniform(new Texture())], ["uCellSize", new Uniform(cellSize)], ["uCharactersCount", new Uniform(characters.length)], ["uColor", new Uniform(new Color(color))], - ["uInvert", new Uniform(invert)] + ["uInvert", new Uniform(invert)], + ["uUseSceneColor", new Uniform(sceneColor)] ]) super("ASCII", fragmentShader, { uniforms }) @@ -192,4 +77,8 @@ export class AsciiEffect extends Effect { texture.needsUpdate = true return texture } + + setUseSceneColor(value) { + this.uniforms.get("uUseSceneColor").value = value; + } } diff --git a/src/effects/glsl/ascii.frag b/src/effects/glsl/ascii.frag index c436476ab..0d4603514 100644 --- a/src/effects/glsl/ascii.frag +++ b/src/effects/glsl/ascii.frag @@ -3,6 +3,7 @@ uniform float uCharactersCount; uniform float uCellSize; uniform bool uInvert; uniform vec3 uColor; +uniform bool uUseSceneColor; const vec2 SIZE = vec2(16.); @@ -32,7 +33,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset; vec4 asciiCharacter = texture2D(uCharacters, charUV); - asciiCharacter.rgb = uColor * asciiCharacter.r; + asciiCharacter.rgb = (uUseSceneColor ? pixelized.rgb : uColor) * asciiCharacter.r; asciiCharacter.a = pixelized.a; if(asciiCharacter.r == 0.0 && asciiCharacter.g == 0.0 && asciiCharacter.b == 0.0) {