fogón

← BibliotecaConceptomormon/oil-paint

Oil paint

m@mormonConcepto, MIT + CC0 (Patch)
v2hace 3 días

Obra original de Patch ↗

Clonar
FOGONfogon pieces add oil-paint
GITgit clone https://git.fogon.gg/mormon/oil-paint.git
REMIXfogon new oil-paint --from oil-paint
ZIPoil-paint.zipBajar
LINKhttps://fogon.gg/piece/35/oil-paint
Controles

Oil paint

De Patch (patch.gg/blendi/oil-paint), traído a Fogón tal cual. Código MIT, arte CC0 1.0: las licencias y los créditos completos van con la pieza (client/oilpaint/LICENSE, public/LICENCE-NOTES.md, public/CREDITS.md). Lo único cambiado para Fogón: la entrada (client/main.ts abre la sala y carga client/demo.js, que es su src/main.js), el aviso de carga (client/block.js en lugar del patch-block de Patch) un JSON que ahora viene dentro del paquete en vez de pedirse aparte, y el mouse (client/figure/input.js): después de un click mira con solo moverlo aunque el navegador no lo pueda atrapar; Esc lo suelta.

En Fogón: la carpeta que te llevás es client/oilpaint (el pincel). Usa three.js: en tu juego, bun add three@0.186.1. La demo es para una persona; la sala solo cuenta quién está mirando.

Draws a three.js scene as thick oil paint, stroke by stroke: every leaf, trunk, patch of ground and fold of cloth is a dab of the brush pinned to the world, so the paint stays on things as you move. The demo is a third-person walk through an autumn wood.

A cloaked wanderer in a painted autumn wood

In the demo: click to look with the mouse, WASD to move, Shift to sprint, Space to jump, the wheel to step the eye back or in, H hides the hint. A gamepad works too. There is no sound.

It is a starting point, not a filter. You can't lay it over a finished game: each thing in your scene is given three mixes of colour and a way for its strokes to run. Start from the wood, change it, replace it.

The idea

Nothing is filtered after the picture is drawn. Everything is a dab: one touch of a loaded brush, in one mixed colour, pinned to a point of the world and turned to lie along a direction of the world (the grain of a trunk, the fall of a cloak).

  • Colour is paint, not light. A thing has three mixes (light, half-tone, shade). Each dab picks one from how lit its spot is, then is mixed a little differently from its neighbours. Nothing is tone-mapped, so you choose colours as they should look on the canvas.
  • The brush is a sheet of eight marks made in code: where there is paint, how thick it stands, and a streak of a second colour. A light from the upper left of the canvas rakes across the thickness.
  • Distance simplifies. Dabs keep between a least and a most size on the canvas, so far things are painted with fewer, larger touches.
  • The ground stays put. Its dabs lie flat on it, real patches of the surface, each at its own small height, so they overlap in one fixed order and never turn with the eye. Every dab belongs to one small square of ground for good. With distance, one in four is drawn larger to stand for its neighbours; as you come nearer it shrinks back where it lies and the others grow in round it. Nothing is swapped for something else.
  • Shade. The still world's shade is drawn once from the sun, and a dab takes it whole, so a shadow's edge is a ragged line of whole dabs. A moving thing's shadow would make dabs flick as it passed, so it goes over the ground as a soft glaze, pixel by pixel.
  • Mist puts distance between one row of things and the next. The one thing that is not a dab is the sun standing in that mist, found by walking each line of sight through the shade.

Use it in a game

It owns the picture: a three.js WebGLRenderer (WebGL2), its own render target and final pass, and every material. Your scene, camera and game loop stay yours. Built on three 0.186.

import * as THREE from 'three';
import { createShared, createBrushSheet, dabMaterial, dabGeometry, dabMesh, groundRings, pinDabs, createShade, createFrame } from './src/oilpaint/index.js';

const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.autoClear = false;
renderer.outputColorSpace = THREE.LinearSRGBColorSpace; // colours are paint: written as they are

const shared = createShared();              // sun, wind, mist, maps: every material reads these
shared.tBrush.value = createBrushSheet();
// shared.tHeight / shared.tLand: your ground's height and a map of what lies where (see src/world/land.js)

// 1. dabs of your own: positions in buffers, colour and direction decided in GLSL
const mesh = dabMesh(dabGeometry(count, { aPos: [positions, 3] }), dabMaterial(shared, {
  head: `in vec3 aPos;`,
  body: `void makeDab(inout Dab d) {
    float lit = sunAt(aPos);
    vec4 r = hash4(float(gl_InstanceID));
    d.pos = aPos;
    d.dir = vec3(0.0, 1.0, 0.0);                       // the way the stroke runs
    d.size = vec2(0.2, 0.5);                           // across and along, metres
    d.c1 = hazed(jitter(paint3(LIGHT, HALF, SHADE, lit, r.x), r.yzw, 0.5), aPos);
    d.c2 = d.c1 * 1.15;                                // the colour streaked into it
  }`,
}));

// 2. a ground with no end (see src/world/floor.js for the leaf carpet and the undergrowth)
const carpet = groundRings(shared, { body: carpetGLSL, per: 3, n: 192, cell0: 0.14, levels: 6, minPx: [5, 9], maxPx: [4000, 4000], cells: [0, 8] });

// 3. paint on any mesh, rigged or not: pins spread over its surface and carried with it
const paint = pinDabs(shared, [[light, half, shade]], [{ mesh: model, count: 500, style: (p, n) => ({ pal: 0, size: [0.06, 0.12], dir: UP }) }]);
scene.add(mesh, carpet.group, paint.mesh);

// 4. shade and the canvas
const shade = createShade(renderer, shared, stillThingsScene);   // the still world, once
const frame = createFrame(renderer, shared);

function tick() {
  shared.uTime.value = time;
  shared.uPixel.value = (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) / 2)) / drawingBufferHeight;
  carpet.follow(camera.position.x, camera.position.z);
  paint.update();                    // after the model has moved
  shade.update(scene, heroPosition); // the moving thing's shadow: draws the meshes on layer 1
  frame.render(scene, camera);
}

In makeDab you have paint3 (the three mixes), jitter (broken colour), hazed (mist), sunAt / woodSunAt (shade), gustAt (wind), heightAt / normalAt (the ground) and hash4. Set d.lie = 1.0 with d.nrm and d.height for a dab that lies on a surface, and d.glaze = 1.0 for one the moving shadow passes over.

What is where

src/oilpaint/The painter: dabs.js (the dab), brush.js (the marks), common.js (shared uniforms and GLSL), ground.js (the steady ground), pins.js (paint on a mesh), shade.js, finish.js (the canvas)
src/world/The wood: land.js (ground and the way through), floor.js (leaf carpet, undergrowth), forest.js (about 3,100 trees: trunks, bark, about 570,000 leaf dabs), sky.js, leaves.js (falling), kick.js (kicked up)
src/figure/The wanderer: Patch's starter man and his movement (man.js, animator.js, player.js, input.js), his hat, and cloak.js, a cloth of points on threads kept off his body and under the hat's brim
src/main.jsThe demo: puts it together, warms it up, talks to Patch's page
tools/Headless stills (shots.mjs), a film (clip.mjs), frame time (shots.mjs --time, fps.mjs), steadiness of the ground (steady.mjs)

Limits

  • three.js on WebGL2 only. It is not a node material and does not run on WebGPU.
  • Everything is opaque: there is no water or glass.
  • Textures on your models are ignored; colour comes from the palettes you give.
  • Measured only on an RTX 4090: about 4.6 ms a frame at 1080p. ?quality=low, or Patch's performance mode, drops the sun in the mist and most of the falling leaves.
  • The demo downloads about 26 MB, most of it the wanderer's clips.
  • The wood is one hour of one day. Walk more than 78 m from the way and you are put back at the start.

Build

npm install, then npm run build: the demo is in dist/ with index.html at the top. node tools/serve.mjs dist 5405 serves it. The tools in tools/ drive it headless with Playwright: set CHROME to a Chromium of your own if Playwright has none.

Credits

Code MIT, art CC0 1.0. The wanderer is Patch's starter man (a MakeHuman figure, CC0) with clips from Quaternius' Universal Animation Library (CC0) and Patch's own (CC0). Physics by Rapier (Apache-2.0). Every part and its source is in LICENCE-NOTES.md.