This commit is contained in:
Ricky Reusser 2021-09-15 22:58:28 -07:00
parent 9543964bf1
commit 3d2f06af09
14 changed files with 662 additions and 32 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
<!DOCTYPE html><html lang="en" dir="ltr"><head><title>Lawson's Klein Bottle</title><meta charset="utf-8"><meta name="application-name" content="Lawson's Klein Bottle">
<meta name="subject" content="3D sterographic projection of a 4D Klein bottle">
<meta name="abstract" content="3D sterographic projection of a 4D Klein bottle">
<meta name="twitter:title" content="Lawson's Klein Bottle">
<meta name="description" content="3D sterographic projection of a 4D Klein bottle">
<meta name="twitter:description" content="3D sterographic projection of a 4D Klein bottle">
<meta name="author" content="Ricky Reusser">
<meta name="twitter:creator" content="Ricky Reusser">
<meta name="twitter:card" content="summary">
<meta property="og:title" content="Lawson's Klein Bottle">
<meta property="og:description" content="3D sterographic projection of a 4D Klein bottle">
<meta property="article:author" content="Ricky Reusser">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" /></head><body><script src="bundle.js"></script><script src="../nav.bundle.js"></script></body></html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -47,7 +47,7 @@ switch (entryFile.type) {
ssr: true,
theme: 'none',
layout: 'none',
transform: ['glslify']
transform: [],//['glslify']
});
idyll.build();
@ -56,7 +56,7 @@ switch (entryFile.type) {
var cpInputDir = path.join(__dirname, '..', projectDir, dir);
var cpOutputDir = path.join(__dirname, '..', outputDir, dir);
if (fs.existsSync(cpInputDir)) {
if (cpInputDir && fs.existsSync(cpInputDir)) {
console.log('copying', dir);
cpr(cpInputDir, cpOutputDir, {});
}
@ -130,7 +130,7 @@ switch (entryFile.type) {
var cpInputDir = path.join(__dirname, '..', projectDir, dir);
var cpOutputDir = path.join(__dirname, '..', outputDir, dir);
if (fs.existsSync(cpInputDir)) {
if (cpInputDir && fs.existsSync(cpInputDir)) {
console.log('copying', dir);
cpr(cpInputDir, cpOutputDir, {});
}

View File

@ -0,0 +1,19 @@
# WIP Lawson's Klein Bottle
It uses the [regl](https://github.com/regl-project/regl) library for interfacing with WebGL and uses [this shader technique](https://observablehq.com/@rreusser/faking-transparency-for-3d-surfaces) for drawing the surface.
There are some shared dependencies a couple directories up, so you'll need to run `npm install` both in the root project directory and in this directory:
```sh
git clone https://github.com/rreusser/explorations.git
cd explorations
npm install
cd posts/lawsons-klein-bottle
npm install
npm start
```
## License
&copy; 2020 Ricky Reusser. MIT License.

View File

@ -0,0 +1,176 @@
const createREGL = require('regl');
const createCamera = require('./regl-turntable-camera');
const meshSurface = require('./mesh-surface-2d');
const mat4create = require('gl-mat4/create');
const mat4multiply = require('gl-mat4/multiply');
const mat4lookAt = require('gl-mat4/lookAt');
const State = require('controls-state');
const GUI = require('controls-gui');
const state = GUI(State({
tau: State.Slider(1, {min: 0, max: 2.0, step: 0.01, label: 'τ'}),
uRange: State.Slider(1, {min: 0, max: 2.0, step: 0.01, label: 'uRange'}),
vRange: State.Slider(1, {min: 0, max: 2.0, step: 0.01, label: 'vRange'}),
}), {
containerCSS: "position:absolute; top:0; right:10px; width:350px; z-index: 1",
});
function createDrawThingy (regl, res) {
const mesh = meshSurface({}, (out, u, v) => {
out[0] = u * 0.999999;
out[1] = v * 0.999999;
}, {
resolution: [res, res],
uDomain: [-Math.PI, Math.PI],
vDomain: [-Math.PI * 0.5, Math.PI * 0.5],
});
return regl({
vert: `
precision highp float;
attribute vec2 uv;
uniform mat4 uProjectionView;
uniform float uTau;
uniform vec2 uRange;
varying vec3 vPosition, vNormal;
varying float vRadius;
varying vec2 vUV;
/*
vec4 quatMult(vec4 q1, vec4 q2) {
return vec4(
(q1.w * q2.x) + (q1.x * q2.w) + (q1.y * q2.z) - (q1.z * q2.y),
(q1.w * q2.y) - (q1.x * q2.z) + (q1.y * q2.w) + (q1.z * q2.x),
(q1.w * q2.z) + (q1.x * q2.y) - (q1.y * q2.x) + (q1.z * q2.w),
(q1.w * q2.w) - (q1.x * q2.x) - (q1.y * q2.y) - (q1.z * q2.z)
);
}
*/
vec3 f(vec2 uv, float tau) {
float tx = tau * uv.x;
vec4 p = vec4(
cos(uv.y) * vec2(cos(uv.x), sin(uv.x)),
sin(uv.y) * vec2(cos(tx), sin(tx))
);
//p.xyz = mat3(uView) * p.xzy;
// Compute the stereographic projection
return p.yzx / (1.0 - p.w);
}
void main () {
vUV = uv;
vec2 uvScaled = uv * uRange;
vUV *= uRange;
vPosition = f(uvScaled, uTau);
vRadius = dot(vPosition, vPosition);
// Taint bad triangles to prevent them from passing through the origin as they cross from -Ininity to Infinity
if (vRadius > 400.0) vPosition /= 0.0;
// Compute the normal via numerical differentiation
const float dx = 5e-3;
vNormal = normalize(cross(
f(uvScaled + vec2(dx / uRange.x, 0), uTau) - vPosition,
f(uvScaled + vec2(0, dx / uRange.y), uTau) - vPosition
));
gl_Position = uProjectionView * vec4(vPosition, 1);
}
`,
frag: `
#extension GL_OES_standard_derivatives : enable
precision highp float;
varying vec3 vPosition, vNormal;
uniform vec3 uEye;
uniform bool uWire;
varying vec2 vUV;
varying float vRadius;
uniform float pixelRatio;
#define PI 3.141592653589
// From https://github.com/rreusser/glsl-solid-wireframe
float gridFactor (vec2 parameter, float width, float feather) {
float w1 = width - feather * 0.5;
vec2 d = fwidth(parameter);
vec2 looped = 0.5 - abs(mod(parameter, 1.0) - 0.5);
vec2 a2 = smoothstep(d * w1, d * (w1 + feather), looped);
return min(a2.x, a2.y);
}
void main () {
if (dot(vPosition, vPosition) > 100.0) discard;
// Shading technique adapted/simplified/customized from: https://observablehq.com/@rreusser/faking-transparency-for-3d-surfaces
vec3 normal = normalize(vNormal);
float vDotN = abs(dot(normal, normalize(vPosition - uEye)));
float vDotNGrad = fwidth(vDotN);
float cartoonEdge = smoothstep(0.75, 1.25, vDotN / (vDotNGrad * 3.0 * pixelRatio));
float sgn = gl_FrontFacing ? 1.0 : -1.0;
float grid = gridFactor(vUV * vec2(2.0, 2.0) * 4.0 / PI, 0.25 * pixelRatio, 1.0);
vec3 baseColor = gl_FrontFacing ? vec3(0.9, 0.2, 0.1) : vec3(0.1, 0.4, 0.8);
float vDotN4 = vDotN * vDotN;
vDotN *= vDotN4;
vDotN *= vDotN4;
float shade = mix(1.0, vDotN4, 0.6) + 0.2;
if (uWire) {
gl_FragColor.rgb = vec3(1);
gl_FragColor.a = mix(0.15, (1.0 - grid) * 0.055, cartoonEdge);
} else {
gl_FragColor = vec4(pow(
mix(baseColor, (0.5 + sgn * 0.5 * normal), 0.4) * cartoonEdge * mix(1.0, 0.6, 1.0 - grid) * shade,
vec3(0.454)),
1.0);
}
}
`,
uniforms: {
uRange: (ctx, props) => [state.uRange, state.vRange],
uTau: regl.prop('tau'),
uWire: regl.prop('wire'),
pixelRatio: regl.context('pixelRatio'),
},
attributes: {uv: mesh.positions},
depth: {enable: (ctx, props) => props.wire ? false : true},
blend: {
enable: (ctx, props) => props.wire ? true : false,
func: {srcRGB: 'src alpha', srcAlpha: 1, dstRGB: 1, dstAlpha: 1},
equation: {rgb: 'reverse subtract', alpha: 'add'}
},
elements: mesh.cells
});
}
const regl = createREGL({extensions: ['OES_standard_derivatives']});
const camera = createCamera(regl, {
distance: 8,
theta: Math.PI * 1.1,
phi: Math.PI * 0.1,
far: 200,
rotateAbountCenter: true
});
const drawTorus = createDrawThingy(regl, 200);
state.$onChanges(camera.taint);
let frame = regl.frame(({tick, time}) => {
camera({
rotationCenter: camera.params.center
}, ({dirty}) => {
if (!dirty) return;
regl.clear({color: [1, 1, 1, 1]});
// Perform two drawing passes, first for the solid surface, then for the wireframe overlayed on top
// to give a fake transparency effect
drawTorus([
{wire: false, tau: state.tau},
{wire: true, tau: state.tau}
]);
});
});

View File

@ -0,0 +1,148 @@
'use strict';
const vec3TransformMat4 = require('gl-vec3/transformMat4');
const interactionEvents = require('normalized-interaction-events');
const assert = require('assert');
module.exports = attachCameraControls;
const RADIANS_PER_HALF_SCREEN_WIDTH = Math.PI * 0.75;
function attachCameraControls (camera, opts) {
opts = opts || {};
var element = camera.element;
var onStart = null;
var onEnd = null;
var onMove = null;
var singletonEventData = {
defaultPrevented: false
};
function localPreventDefault () {
singletonEventData.defaultPrevented = true;
}
function resetLocalPreventDefault () {
singletonEventData.defaultPrevented = false;
}
function providePreventDefault (ev) {
ev.defaultPrevented = singletonEventData.defaultPrevented;
ev.preventDefault = function () {
ev.defaultPrevented = true;
localPreventDefault();
};
return ev;
}
var v = [0, 0, 0];
var xy = [0, 0];
function transformXY(ev) {
v[0] = ev.x;
v[1] = ev.y;
v[2] = 0;
if (opts.invViewportShift) {
vec3TransformMat4(v, v, invViewportShift);
}
xy[0] = v[0];
xy[1] = v[1];
return xy;
}
interactionEvents(element)
.on('wheel', function (ev) {
ev.originalEvent.preventDefault();
camera.zoom(ev.x0, ev.y0, Math.exp(-ev.dy) - 1.0);
})
.on('mousedown', function (ev) {
resetLocalPreventDefault();
ev = providePreventDefault(ev);
onStart && onStart(ev);
ev.originalEvent.preventDefault();
})
.on('mousemove', function (ev) {
ev = providePreventDefault(ev);
onMove && onMove(ev);
if (ev.defaultPrevented) return;
if (!ev.active || ev.buttons !== 1) return;
if (ev.mods.alt) {
camera.zoom(ev.x0, ev.y0, Math.exp(ev.dy) - 1.0);
ev.originalEvent.preventDefault();
} else if (ev.mods.shift) {
camera.pan(ev.dx, ev.dy);
ev.originalEvent.preventDefault();
} else if (ev.mods.meta) {
camera.pivot(ev.dx, ev.dy);
ev.originalEvent.preventDefault();
} else {
camera.rotate(
-ev.dx * RADIANS_PER_HALF_SCREEN_WIDTH,
-ev.dy * RADIANS_PER_HALF_SCREEN_WIDTH
);
ev.originalEvent.preventDefault();
}
})
.on('mouseup', function (ev) {
ev.originalEvent.preventDefault();
resetLocalPreventDefault();
ev = providePreventDefault(ev);
onEnd && onEnd(ev);
})
.on('touchstart', function (ev) {
ev.originalEvent.preventDefault();
ev = providePreventDefault(ev);
onStart && onStart(ev);
})
.on('touchmove', function (ev) {
ev = providePreventDefault(ev);
onMove && onMove(ev);
if (ev.defaultPrevented) return;
if (!ev.active) return;
camera.rotate(
-ev.dx * RADIANS_PER_HALF_SCREEN_WIDTH,
-ev.dy * RADIANS_PER_HALF_SCREEN_WIDTH
);
ev.originalEvent.preventDefault();
})
.on('touchend', function (ev) {
ev.originalEvent.preventDefault();
resetLocalPreventDefault();
ev = providePreventDefault(ev);
onEnd && onEnd(ev);
})
.on('pinchmove', function (ev) {
if (!ev.active) return;
transformXY(ev);
camera.zoom(xy[0], xy[1], 1 - ev.zoomx);
camera.pan(ev.dx, ev.dy);
ev.originalEvent.preventDefault();
})
.on('pinchstart', function (ev) {
ev.originalEvent.preventDefault();
});
onStart = opts.onStart;
onMove = opts.onMove;
onEnd = opts.onEnd;
return {
setInteractions: function (interactions) {
assert(interactions);
onStart = interactions.onStart;
onEnd = interactions.onEnd;
onMove = interactions.onMove;
}
};
}

View File

@ -0,0 +1,106 @@
'use strict';
function assert (condition, message) {
if (!condition) throw new Error(message);
}
var DEFAULT_RESOLUTION = 30;
var tmp1 = [0.0, 0.0, 0.0];
var tmp2 = [0.0, 0.0, 0.0];
var tmp3 = [0.0, 0.0, 0.0];
module.exports = function (meshData, surfaceFn, opts) {
var i, j, u, v, index, nbUFaces, nbVFaces;
opts = opts || {};
var res = opts.resolution || DEFAULT_RESOLUTION;
var nbUFaces = Array.isArray(opts.resolution) ? opts.resolution[0] : res;
var nbVFaces = Array.isArray(opts.resolution) ? opts.resolution[1] : res;
var uDomain = opts.uDomain === undefined ? [0, 1] : opts.uDomain;
var vDomain = opts.vDomain === undefined ? [0, 1] : opts.vDomain;
meshData = meshData || {};
var nbBoundaryAdjustedUFaces = nbUFaces;
var nbBoundaryAdjustedVFaces = nbVFaces;
if (!opts.uClosed) nbBoundaryAdjustedUFaces += 1;
if (!opts.vClosed) nbBoundaryAdjustedVFaces += 1;
var nbPositions = nbBoundaryAdjustedUFaces * nbBoundaryAdjustedVFaces;
var positionDataLength = nbPositions * 2;
var positions = meshData.positions = meshData.positions || new Float32Array(positionDataLength);
assert(positions.length, positionDataLength, 'Incorrect number of positions in pre-allocated array');
var nbFaces = nbUFaces * nbVFaces * 2;
var cellDataLength = nbFaces * 3;
var cells = meshData.cells = meshData.cells || new Int16Array(cellDataLength);
assert(cells.length, cellDataLength, 'Incorrect number of cells in pre-allocated array');
if (opts.attributes) {
meshData.attributes = {};
var attrSize = {};
var attributeKeys = Object.keys(opts.attributes);
for (i = 0; i < attributeKeys.length; i++) {
var key = attributeKeys[i];
var attrFn = opts.attributes[key];
var test = [];
attrFn(test, uDomain[0], vDomain[0]);
attrSize[key] = test.length;
var attrDataLength = nbPositions * attrSize[key];
meshData.attributes[key] = meshData.attributes[key] || new Float32Array(attrDataLength);
assert(meshData.attributes[key].length, attrDataLength, 'Incorrect attr size in pre-allocated array for attr ' + key);
}
}
for (i = 0; i < nbBoundaryAdjustedUFaces; i++) {
u = uDomain[0] + (uDomain[1] - uDomain[0]) * i / nbUFaces;
for (j = 0; j < nbBoundaryAdjustedVFaces; j++) {
v = vDomain[0] + (vDomain[1] - vDomain[0]) * j / nbVFaces;
index = 2 * (i + nbBoundaryAdjustedUFaces * j);
surfaceFn(tmp1, u, v);
positions[index + 0] = tmp1[0];
positions[index + 1] = tmp1[1];
if (attributeKeys) {
for (var k = 0; k < attributeKeys.length; k++) {
var key = attributeKeys[k];
var attrFn = opts.attributes[key];
attrFn(tmp1, u, v);
var attrIndex = (i + nbBoundaryAdjustedUFaces * j) * attrSize[key];
var attrData = meshData.attributes[key];
for (var l = 0; l < attrSize[key]; l++) {
attrData[attrIndex + l] = tmp1[l];
}
}
}
}
}
var faceIndex = 0;
for (i = 0; i < nbUFaces; i++) {
var iPlusOne = i + 1;
if (opts.uClosed) iPlusOne = iPlusOne % nbUFaces;
for (j = 0; j < nbVFaces; j++) {
var jPlusOne = j + 1;
if (opts.vClosed) jPlusOne = jPlusOne % nbVFaces;
cells[faceIndex++] = i + nbBoundaryAdjustedUFaces * j;
cells[faceIndex++] = iPlusOne + nbBoundaryAdjustedUFaces * j;
cells[faceIndex++] = iPlusOne + nbBoundaryAdjustedUFaces * jPlusOne;
cells[faceIndex++] = i + nbBoundaryAdjustedUFaces * j;
cells[faceIndex++] = iPlusOne + nbBoundaryAdjustedUFaces * jPlusOne;
cells[faceIndex++] = i + nbBoundaryAdjustedUFaces * jPlusOne;
}
}
return meshData;
};

View File

@ -0,0 +1,6 @@
{
"title": "Lawson's Klein Bottle",
"description": "3D sterographic projection of a 4D Klein bottle",
"order": 3200,
"image": "http://rreusser.github.io/src/src/lawsons-klein-bottle/thumbnail.jpg"
}

View File

@ -0,0 +1,83 @@
'use strict';
var mat4create = require('gl-mat4/create');
var mat4multiply = require('gl-mat4/multiply');
var createCamera = require('inertial-turntable-camera');
var createInteractions = require('./interactions');
var RADIANS_PER_HALF_SCREEN_WIDTH = Math.PI * 2 * 0.4;
module.exports = function createReglCamera (regl, opts) {
var element = regl._gl.canvas;
element.addEventListener('wheel', event => event.preventDefault());
function getAspectRatio () {
return element.clientWidth / element.clientHeight;
}
var camera = createCamera(Object.assign({}, {
aspectRatio: getAspectRatio(),
}, opts || {}));
var mProjectionView = mat4create();
var setCameraUniforms = regl({
context: {
projection: () => camera.state.projection,
view: () => camera.state.view,
viewInv: () => camera.state.viewInv,
eye: () => camera.state.eye,
},
uniforms: {
uProjectionView: ctx => mat4multiply(mProjectionView, ctx.projection, ctx.view),
uProjection: regl.context('projection'),
uView: regl.context('view'),
uEye: regl.context('eye'),
}
});
function invokeCamera (props, callback) {
if (!callback) {
callback = props;
props = {};
}
camera.tick(props);
setCameraUniforms(function () {
callback(camera.state, camera.params);
});
}
invokeCamera.taint = camera.taint;
invokeCamera.resize = camera.resize;
invokeCamera.tick = camera.tick;
invokeCamera.setUniforms = setCameraUniforms;
invokeCamera.rotate = camera.rotate;
invokeCamera.pan = camera.pan;
invokeCamera.pivot = camera.pivot;
invokeCamera.zoom = camera.zoom;
Object.defineProperties(invokeCamera, {
state: {
get: function () { return camera.state; },
set: function (value) { camera.state = value; }
},
params: {
get: function () { return camera.params; },
set: function (value) { camera.params = value; }
},
element: {
get: function () { return element; }
},
});
window.addEventListener('resize', function () {
camera.resize(getAspectRatio());
}, false);
camera.element = regl._gl.canvas;
createInteractions(camera);
return invokeCamera;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB