diff --git a/manual/assets/js/src/demos/ascii.js b/manual/assets/js/src/demos/ascii.js deleted file mode 100644 index 940cb74c9..000000000 --- a/manual/assets/js/src/demos/ascii.js +++ /dev/null @@ -1,155 +0,0 @@ -import { - CubeTextureLoader, - FogExp2, - LoadingManager, - PerspectiveCamera, - Scene, - SRGBColorSpace, - WebGLRenderer -} from "three"; - -import { - ASCIIEffect, - ASCIITexture, - BlendFunction, - 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, 10, 1); - controls.lookAt(0, 10, -1); - - // Scene, Lights, Objects - - const scene = new Scene(); - scene.fog = new FogExp2(0x373134, 0.06); - scene.background = assets.get("sky"); - scene.add(Domain.createLights()); - scene.add(Domain.createEnvironment(scene.background)); - scene.add(Domain.createActors(scene.background)); - - // Post Processing - - const composer = new EffectComposer(renderer, { - multisampling: Math.min(4, renderer.capabilities.maxSamples) - }); - - const effect = new ASCIIEffect({ - asciiTexture: new ASCIITexture({ - characters: " .:,'-^=*+?!|0#X%WM@", - font: "Arial", - fontSize: 54, - size: 1024, - maxCharsPerRow: 16 - }), - cellSize: 12, - inverted: false - }); - - composer.addPass(new RenderPass(scene, camera)); - composer.addPass(new EffectPass(camera, effect)); - - // Settings - - const params = { useSceneColor: true }; - - 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, "inverted"); - folder.addBinding(effect, "cellSize", { min: 2, max: 24, step: 2 }); - folder.addBinding(effect, "color", { color: { type: "float" } }); - folder.addBinding(params, "useSceneColor").on("change", - (e) => void (effect.color = e.value ? null : effect.color.getHex())); - - folder.addBinding(effect.blendMode.opacity, "value", { label: "opacity", min: 0, max: 1, step: 0.01 }); - folder.addBinding(effect.blendMode, "blendFunction", { options: BlendFunction }); - - // 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/assets/js/src/demos/noise.ts b/manual/assets/js/src/demos/noise.ts new file mode 100644 index 000000000..46dcaff9c --- /dev/null +++ b/manual/assets/js/src/demos/noise.ts @@ -0,0 +1,159 @@ +import { + CubeTextureLoader, + LoadingManager, + PerspectiveCamera, + PointLight, + SRGBColorSpace, + Scene, + Texture, + WebGLRenderer +} from "three"; + +import { + ClearPass, + EffectPass, + GeometryPass, + NoiseEffect, + RenderPipeline, + ScreenBlendFunction, + ToneMappingEffect +} from "postprocessing"; + +import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; +import { Pane } from "tweakpane"; +import { SpatialControls } from "spatial-controls"; +import * as Utils from "../utils/index.js"; + +function load(): Promise> { + + const assets = new Map(); + const loadingManager = new LoadingManager(); + const gltfLoader = new GLTFLoader(loadingManager); + const cubeTextureLoader = new CubeTextureLoader(loadingManager); + + return new Promise>((resolve, reject) => { + + loadingManager.onLoad = () => resolve(assets); + loadingManager.onError = (url) => reject(new Error(`Failed to load ${url}`)); + + cubeTextureLoader.load(Utils.getSkyboxUrls("space-02", ".jpg"), (t) => { + + t.colorSpace = SRGBColorSpace; + assets.set("sky", t); + + }); + + gltfLoader.load( + `${document.baseURI}models/spaceship-corridor/spaceship-corridor.glb`, + (gltf) => assets.set("model", gltf) + ); + + }); + +} + +window.addEventListener("load", () => void load().then((assets) => { + + // Renderer + + const renderer = new WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: false + }); + + renderer.setPixelRatio(window.devicePixelRatio); + renderer.debug.checkShaderErrors = Utils.isLocalhost; + + // 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, 1, 8.25); + controls.lookAt(0, 0.6, -1); + + // Scene, Lights, Objects + + const scene = new Scene(); + const skyMap = assets.get("sky")! as Texture; + scene.background = skyMap; + scene.environment = skyMap; + + const light0 = new PointLight(0xbeefff, 20, 12, 2); + light0.position.set(0, 0.3, -5); + scene.add(light0); + + const light1 = new PointLight(0xffedde, 6, 0, 2); + light1.position.set(0, 1.3, 5); + scene.add(light1); + + const gltf = assets.get("model") as GLTF; + Utils.setAnisotropy(gltf.scene, Math.min(8, renderer.capabilities.getMaxAnisotropy())); + scene.add(gltf.scene); + + // Post Processing + + const effect = new NoiseEffect(); + + effect.blendMode.blendFunction = new ScreenBlendFunction(); + effect.blendMode.opacity = 0.5; + + const pipeline = new RenderPipeline(renderer); + pipeline.add( + new ClearPass(), + new GeometryPass(scene, camera, { samples: 4 }), + new EffectPass(effect, new ToneMappingEffect()) + ); + + + // Settings + + const container = document.getElementById("viewport")!; + const pane = new Pane({ container: container.querySelector(".tp")! }); + const fpsGraph = Utils.createFPSGraph(pane); + + const folder = pane.addFolder({ title: "Settings" }); + folder.addBinding(effect, "premultiply"); + + + Utils.addBlendModeBindings(folder, effect.blendMode); + + // Resize Handler + + function onResize(): void { + + const width = container.clientWidth; + const height = container.clientHeight; + camera.aspect = width / height; + camera.fov = Utils.calculateVerticalFoV(90, Math.max(camera.aspect, 16 / 9)); + camera.updateProjectionMatrix(); + pipeline.setSize(width, height); + + } + + window.addEventListener("resize", onResize); + onResize(); + + // Render Loop + + pipeline.compile().then(() => { + + container.prepend(renderer.domElement); + + renderer.setAnimationLoop((timestamp) => { + + fpsGraph.begin(); + controls.update(timestamp); + pipeline.render(timestamp); + fpsGraph.end(); + + }); + + }).catch((e) => console.error(e)); + +})); diff --git a/manual/content/demos/special-effects/noise.en.md b/manual/content/demos/special-effects/noise.en.md index b120fcbdd..a1989dad1 100644 --- a/manual/content/demos/special-effects/noise.en.md +++ b/manual/content/demos/special-effects/noise.en.md @@ -2,7 +2,7 @@ layout: single collection: sections title: Noise -draft: true +draft: false menu: demos: parent: special-effects diff --git a/src/effects/NoiseEffect.ts b/src/effects/NoiseEffect.ts new file mode 100644 index 000000000..22ca068e8 --- /dev/null +++ b/src/effects/NoiseEffect.ts @@ -0,0 +1,92 @@ +import { ScreenBlendFunction } from "./blending/index.js"; +import { Effect } from "./Effect.js"; + +import fragmentShader from "./shaders/noise.frag"; + +/** + * A noise effect options + * + * @category Effects + */ + +export interface NoiseEffectOptions { + /** + * The blend function of this effect. + */ + blendFunction?: ScreenBlendFunction; + + /** + * Whether the noise should be multiplied with the input colors prior to blending. + */ + premultiply?: boolean; +} + + +/** + * A noise effect. + * + * @category Effects + */ + + +export class NoiseEffect extends Effect { + + /** + * Constructs a new noise effect. + * + */ + + constructor({ + blendFunction, + premultiply = false + }: NoiseEffectOptions = {}) { + + super("NoiseEffect"); + + this.fragmentShader = fragmentShader; + this.blendMode.blendFunction = blendFunction ?? new ScreenBlendFunction(); + + + this.premultiply = premultiply; + + } + + /** + * Indicates whether noise will be multiplied with the input colors prior to blending. + * + */ + + get premultiply(): boolean { + + return this.input.defines.has("PREMULTIPLY"); + + } + + + /** + * Enables or disables premultiplication of the noise. + * + */ + + set premultiply(value: boolean) { + + if(this.premultiply !== value) { + + if(value) { + + this.input.defines.set("PREMULTIPLY", "1"); + + } else { + + this.input.defines.delete("PREMULTIPLY"); + + } + + this.setChanged(); + + } + + } + + +} diff --git a/src/effects/index.ts b/src/effects/index.ts index a2c057533..5498085a1 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -9,6 +9,7 @@ export * from "./HalftoneEffect.js"; export * from "./LensDistortionEffect.js"; export * from "./LUT1DEffect.js"; export * from "./LUT3DEffect.js"; +export * from "./NoiseEffect.js"; export * from "./ScanlineEffect.js"; export * from "./SMAAEffect.js"; export * from "./TextureEffect.js"; diff --git a/src/effects/shaders/noise.frag b/src/effects/shaders/noise.frag new file mode 100644 index 000000000..e327b25a0 --- /dev/null +++ b/src/effects/shaders/noise.frag @@ -0,0 +1,15 @@ +vec4 mainImage(const in vec4 inputColor, const in vec2 uv, const in GData gData) { + + vec3 noise = vec3(rand(uv * (1.0 + time))); + + #ifdef PREMULTIPLY + + return vec4(min(inputColor.rgb * noise, vec3(1.0)), inputColor.a); + + #else + + return vec4(noise, inputColor.a); + + #endif + +} \ No newline at end of file