Skip to content

Instantly share code, notes, and snippets.

@nkint
Last active July 26, 2020 13:20
Show Gist options
  • Save nkint/bc4ad632e7c57fc14ef3a2a9654acf96 to your computer and use it in GitHub Desktop.
Save nkint/bc4ad632e7c57fc14ef3a2a9654acf96 to your computer and use it in GitHub Desktop.
umbrella webgl-to-pixel
/*
yarn add @thi.ng/hdom @thi.ng/pixel @thi.ng/hdom-canvas @thi.ng/geom @thi.ng/transducers @thi.ng/hdom-components @thi.ng/webgl @thi.ng/memoize @thi.ng/shader-ast @thi.ng/shader-ast-stdlib webgl-shadertoy
*/
import { renderOnce } from "@thi.ng/hdom";
import { PackedBuffer, canvas2d, ABGR8888, GRAY8 } from "@thi.ng/pixel";
import { canvas as hdomCanvas } from "@thi.ng/hdom-canvas";
import * as g from "@thi.ng/geom";
import { range, map, normRange } from "@thi.ng/transducers";
import { glCanvas } from "@thi.ng/webgl";
import {
vec4,
Vec2Sym,
FloatSym,
sym,
vec2,
float,
ret,
vec3,
$xy,
div,
sin,
mul,
distance,
} from "@thi.ng/shader-ast";
import { fit1101 } from "@thi.ng/shader-ast-stdlib";
import { shaderToy } from "@thi.ng/webgl-shadertoy";
const W = 200;
const H = 200;
const { canvas, gl } = glCanvas({
width: W,
height: H,
autoScale: false,
parent: document.body,
version: 1,
});
const heightMap = shaderToy({
canvas,
gl,
main: (gl, unis) => {
let st: Vec2Sym;
let color: FloatSym;
return [
(st = sym(div($xy(gl.gl_FragCoord), unis.resolution))),
(color = sym(
fit1101(sin(mul(float(16), distance(st, vec2(0.5)))))
)),
ret(vec4(vec3(color), 1)),
];
},
});
heightMap.update();
const buf = new PackedBuffer(W, H, ABGR8888);
gl.readPixels(
0,
0,
W,
H,
// don't use gl.RGB since incompatible w/ PackedBuffer
// RGB has a stride of only 3 bytes, there's no equivalent typed array for that size
// hence we need to read 4 channels
gl.RGBA,
gl.UNSIGNED_BYTE,
// read directly into pixel buffer's memory
new Uint8Array(buf.pixels.buffer)
);
const canvas2 = canvas2d(W, H, document.body);
buf.flipY().blitCanvas(canvas2.canvas);
const grayBuf = buf.as(GRAY8);
const app = () => {
return [
"div.flex",
["div.ba.w5.h5", "here I like to show webgl-shadertoy heightmap"],
["div.ba.w5.h5", "here I like to show pixel packed buffer"],
[
hdomCanvas,
{
width: W,
height: H,
class: "ba b--moon-gray",
},
g.ellipse([100, 100], 25, { stroke: "black" }),
g.ellipse([100, 100], 95, { stroke: "black" }),
...map(
(yNorm) =>
g.group({}, [
...map((x) => {
const y = yNorm * grayBuf.height;
const value = grayBuf?.getAt(x, y);
const fill = `rgb(${value}, ${value}, ${value})`;
return g.rect([x, y], [1, 2], {
fill,
});
}, range(grayBuf.width)),
]),
normRange(10)
),
],
];
};
renderOnce(app());
if (process.env.NODE_ENV !== "production") {
const hot = (<any>module).hot;
hot && hot.dispose(() => {});
}
@nkint
Copy link
Author

nkint commented Jul 25, 2020

image

@postspectacular
Copy link

postspectacular commented Jul 26, 2020

Here's an updated & animated version with new comments:

import * as g from "@thi.ng/geom";
import { start } from "@thi.ng/hdom";
import { canvas as hdomCanvas } from "@thi.ng/hdom-canvas";
import { canvas2D, canvasWebGL } from "@thi.ng/hdom-components";
import { ABGR8888, PackedBuffer } from "@thi.ng/pixel";
import {
    $xy,
    distance,
    div,
    float,
    FloatSym,
    madd,
    mul,
    ret,
    sin,
    sym,
    vec2,
    Vec2Sym,
    vec3,
    vec4,
} from "@thi.ng/shader-ast";
import { fit1101 } from "@thi.ng/shader-ast-stdlib";
import { map, range2d } from "@thi.ng/transducers";
import {
    shaderToy,
    ShaderToy,
    ShaderToyUniforms,
} from "@thi.ng/webgl-shadertoy";
import { U24 } from "@thi.ng/strings";

const W = 200;
const H = 200;

const app = () => {
    let heightMap: ShaderToy<ShaderToyUniforms>;
    const DPR = window.devicePixelRatio || 1;
    // since canvasWebGL component will adjust for DPR on retina displays,
    // need to scale pixel buffer accordingly
    const buf = new PackedBuffer(W * DPR, H * DPR, ABGR8888);
    // WebGL canvas component
    const canvasComp1 = canvasWebGL({
        init(el, gl) {
            // initialize shadertoy in init
            heightMap = shaderToy({
                gl,
                canvas: el,
                main: (gl, unis) => {
                    let st: Vec2Sym;
                    let color: FloatSym;
                    return [
                        (st = sym(div($xy(gl.gl_FragCoord), unis.resolution))),
                        (color = sym(
                            fit1101(
                                sin(
                                    mul(
                                        float(16),
                                        // use time uniform to create animated version
                                        madd(
                                            unis.time,
                                            float(0.001),
                                            distance(st, vec2(0.5))
                                        )
                                    )
                                )
                            )
                        )),
                        ret(vec4(vec3(color), 1)),
                    ];
                },
            });
        },
        update(el, gl, _, time) {
            if (!heightMap) return;
            heightMap.update(time);
            gl.readPixels(
                0,
                0,
                el.width,
                el.height,
                // don't use gl.RGB since incompatible w/ PackedBuffer
                // RGB has a stride of only 3 bytes, there's no equivalent typed array for that size
                // hence we need to read 4 channels
                gl.RGBA,
                gl.UNSIGNED_BYTE,
                // read directly into pixel buffer's memory
                new Uint8Array(buf.pixels.buffer)
            );
            buf.flipY();
        },
    });
    // 2D canvas component to show pixel buffer
    const canvasComp2 = canvas2D({
        update(el) {
            buf.blitCanvas(el);
        },
    });
    return () => [
        "div.flex",
        ["div.ba.w5.h5", [canvasComp1, { width: W, height: H }]],
        ["div.ba.w5.h5", [canvasComp2, { width: W, height: H }]],
        [
            hdomCanvas,
            {
                width: W,
                height: H,
                class: "ba b--moon-gray",
            },
            g.group({ stroke: "black" }, [
                g.circle([100, 100], 25),
                g.circle([100, 100], 95),
            ]),
            ...map(
                ([x, y]) => {
                    // no need for GRAY buffer conversion, just read a single channel instead...
                    // (here channel=3 aka red channel in ABGR)
                    // need to scale XY coord with DPR for lookup in buffer
                    const value = buf.getChannelAt(x * DPR, y * DPR, 3);
                    // spread channel value to RGB hex, e.g. 0x7f * 0x010101 => 0x7f7f7f
                    // U24 is a 24bit hex formatter
                    const fill = `#${U24(value * 0x010101)}`;
                    // no need for using geom here, faster using hiccup directly
                    return ["rect", { fill }, [x, y], 1, 4];
                },
                // XY iterator with different step sizes for X & Y
                range2d(0, W, 0, H, 1, H / 10)
            ),
        ],
    ];
};

start(app());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment