add new blog posts

This commit is contained in:
Joshua Goins 2023-07-02 20:19:13 -04:00
parent 18ae419281
commit 961eeb75e7
14 changed files with 72168 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -1,34 +1,35 @@
--- ---
title: "Graphics Dump: Drawing debug cubes" title: "Graphics Dump: Drawing debug cubes"
date: 2023-06-28 date: 2023-07-03
draft: true draft: true
summary: "When working on my engine, I wanted to clean up my debug gizmos a bit. The first thing to tackle is drawing bounding boxes!" summary: "When working on my engine, I wanted to clean up my debug gizmos a bit. The first thing to tackle is drawing bounding boxes!"
tags: tags:
- C++
- Math - Math
- GLSL
series: series:
- Graphics Dump - Graphics Dump
threejs: true
math: true
--- ---
When writing debug graphics, one of the most common shapes you might use are cubes. Usually to represent bounding boxes, but they're really versatile. I'll be showcasing a really simple technique you can implement to draw prettier debug boxes. Normally if you try to render a cube and only draw the wireframe, you end up with something like this: When writing debug graphics, one of the most common shapes you draw are cubes. They usually represent bounding boxes, but I think they're really versatile. I'll be showcasing a way you can draw prettier debug boxes. Normally if you try to render a cube and only draw the wireframe, you end up with something like this:
"picture of a cube" {{< three-scene "scene.js" "scene" >}}
The diagonals are unnecessary here, and just add to the visual noise. That's something you typically want to avoid, especially in already packed debug screens. However, I found a pretty neat trick for removing these without having to change the vertex data. I just think it's a neat trick, even if it's a little odd. I don't remember if I made this up originally or found it somewhere else, some precursory searches didn't turn up much. The diagonal lines are unnecessary and add visual noise. That's something you typically want to avoid, especially when in a debug view where the screen is already packed. However, I found a pretty neat trick for removing these without having to change the vertex data. I don't remember if I made this up originally or found it somewhere else, some precursory searches didn't turn up much.
"cube positions" Let's start by breaking it down to one face, in two dimensions:
You might be thinking of checking the vertex positions, comparing whether or not they're -1 or 1, and so on. But this won't work, or you'll end up with lopsided and complex logic. We can think ![A cube visualized on a 2D grid.](grid.webp)
even simpler. Let's start by breaking it down just to one face, in two dimensions:
What is the _real_ difference between the outer edges and the diagonal? I'll give you a hint, it's not about the _position_ but the change in position. Point A `(~0.25, ~0.25)` has one component at the maximum, on the digonal. Point B `(1, 1)` has two components at the maximums because it's situated in the corner. Point C `(-1, 0)` is on the left side. We want to filter out A, but keep B and C since those make up the outer edges and corners. Basically, we need it to be on one or more maximums and if they are in between (like `0.5`) then it needs to go.
Yes, it's that simple - we basically need to check if at least two components are not 0. In shader code, it will look something like this: In real GLSL, it would look something like this:
```glsl ```glsl
bool is_x_okay = abs(inPosition.x) != 1.0; bool is_x_okay = abs(position.x) != 1.0;
bool is_y_okay = abs(inPosition.y) != 1.0; bool is_y_okay = abs(position.y) != 1.0;
bool is_z_okay = abs(inPosition.z) != 1.0; bool is_z_okay = abs(position.z) != 1.0;
int num_components = 0; int num_components = 0;
if (is_x_okay) { if (is_x_okay) {
@ -48,4 +49,36 @@ if (num_components < 2) {
} }
``` ```
However this looks bad, we can definitely write this better. We have functions like notEqual, abs, and length to do component-wise comparisons.z This is pretty disgusting, not only for the programmer but likely for the poor GPU too. Luckily, GLSL has all of the tools necessary to write this more compactly. A better version could be written like this:
```glsl
void main() {
if (length(vec3(notEqual(abs(position), vec3(1.0)))) > 1.0) {
discard;
} else {
gl_FragColor = vec4(1, 0, 0, 1);
}
}
```
Let's break this down, first we can simplify the many equality checks in the beginning with notEqual:
```glsl
notEqual(abs(inPosition), vec3(1.0))
```
This is component-wise, so it has the same exact meaning as before. We want to perform a length check on this, hence the `vec3` cast. Why do we do a length cast? It's the easiest way to detect how many booleans are true, I'm not sure of a better way. For example:
* `(1, 0, 0)` has a length of 1.
* `(1, 1, 0)` has a length of $\sqrt{2}$.
* `(1, 1, 1)` has a lenth of $\sqrt{3}$.
Hence, if we want to check if 0 or 1 components (but not more) we want a length less than 1. And here is the result:
{{< include-shader "wireframe.frag.glsl" "wire-frag" >}}
{{< include-shader "wireframe.vert.glsl" "wire-vert" >}}
{{< three-scene "scene2.js" "scene2" >}}
Since this is using `GL_LINES` (or your API equivalent) you can use the line width - if supported by your GPU - to modify how the lines look. Is this is a little convoluted? Maybe, but I like how it's all neatly integrated into just the fragment stage. I think this is good enough for debug purposes at least.

View file

@ -0,0 +1,41 @@
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
//renderer.setSize( window.innerWidth, window.innerHeight );
document.getElementById('scene').appendChild( renderer.domElement );
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
material.wireframe = true;
const cube = new THREE.Mesh( geometry, material );
scene.add(cube);
camera.position.z = 5;
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
function animate() {
requestAnimationFrame( animate );
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize);
resizeObserver.observe(renderer.domElement, {box: 'content-box'});

View file

@ -0,0 +1,45 @@
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
//renderer.setSize( window.innerWidth, window.innerHeight );
document.getElementById('scene2').appendChild(renderer.domElement);
const material = new THREE.ShaderMaterial({
vertexShader: document.getElementById('wire-vert').textContent,
fragmentShader: document.getElementById('wire-frag').textContent
});
material.wireframe = true;
const geometry = new THREE.BoxGeometry(2, 2, 2);
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
function resizeCanvasToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
function animate() {
requestAnimationFrame( animate );
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize);
resizeObserver.observe(renderer.domElement, {box: 'content-box'});

View file

@ -0,0 +1,9 @@
varying vec3 inPosition;
void main() {
if (length(vec3(notEqual(abs(inPosition), vec3(1.0)))) > 1.0) {
discard;
} else {
gl_FragColor = vec4(0, 1, 0, 1);
}
}

View file

@ -0,0 +1,7 @@
varying vec3 inPosition;
void main() {
inPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

View file

@ -0,0 +1,94 @@
---
title: "Integrating KaTeX and Three.js into Hugo"
date: 2023-07-04
draft: true
tags:
- GLSL
- WebGL
- Math
- Hugo
threejs: true
math: true
---
My [last blog post]({{< ref "drawing-simple-cubes" >}}) integrates three.js and KaTeX into the article itself, which was a first for me. I want to use these libraries in future posts, so I want it to be easy and reusable.
# Three.js
If you aren't familiar, [three.js](https://threejs.org/) is an easy to use WebGL framework/engine and does a lot of useful work out of the box. When you just want to display a mesh, or a primitive and slap some materials on it you can't really get anything better. However, it wasn't very straightforward to integrate it and I also want to use custom GLSL shaders that complicates things a bit.
I self-host my own JavaScript files, and the easiest way to grab a ready-to-use distribution of three.js is from the repository under the "build" directory. Three.js complains in the console that it's not supported, although I'm not familiar what's the better way. To enable three.js for a specific post, I enable it via a parameter in the frontmatter:
```yaml
---
title: "Integrating KaTeX and Three.js into Hugo"
threejs: true
---
```
And then in `single.html` (or the layout you're using for posts) I check for the parameter and insert the JavaScript:
```go
{{ if .Params.threejs }}
{{ $threejs := resources.Get "js/three.js" }}
{{ if hugo.IsProduction }}
{{ $threejs = $threejs | minify | fingerprint | resources.PostProcess }}
{{ end }}
<script src="{{ $threejs.RelPermalink }}" integrity="{{ $threejs.Data.Integrity }}"></script>
{{ end }}
```
That's only half the battle, as we still need to set up the scene and the render loop. To do this, I put the JavaScript files in the page directory. Then using this shortcode I insert a three.js scene inline:
```go
{{ $path := .Get 0 }}
{{ $id := .Get 1 }}
<div class="threejs-canvas" id="{{ $id }}"></div>
<script type="module" src="{{ $path }}"></script>
```
This adds a container and the script itself, and can be used as so:
```
{ {< three-scene "scene.js" "scene" >} }
```
("scene.js" is of course the script path, and "scene" is the name of the container.)
On the three.js side, we query for the container and add the canvas to it:
```javascript
document.getElementById('scene').appendChild(renderer.domElement);
```
I add some CSS to the container to take up 50% of the article width, so we need some way to automatically tell three.js to resize as the canvas changes. First, add a ResizeObserver that calls a `resize()` function:
```javascript
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(renderer.domElement, {box: 'content-box'});
```
The resize function is as follows, which is updating the renderer size and the camera aspect ratio:
```javascript
function resize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
```
Right now this has to be set up for each and every JavaScript file, but I should move it into a common file for all of them later down the road. If you want to
## KaTeX
[KaTeX](https://katex.org/) is a LaTeX-compatible JavaScript math renderer, and so much easier to integrate than three.js. There's three files we need to include, `katex.js`, `katex.css` and `auto-render.js` (from the `contrib` folder).
# Credits
* This [StackOverflow answer](https://stackoverflow.com/a/45046955) explaining how to inform three.js about canvas resizes.
* ["Math Typesetting in Hugo"](https://mertbakir.gitlab.io/hugo/math-typesetting-in-hugo/) for a guide on how to use KaTeX.

File diff suppressed because it is too large Load diff

View file

@ -502,3 +502,9 @@ model-viewer {
padding-right: 0.5em; padding-right: 0.5em;
flex-grow: 1; flex-grow: 1;
} }
.threejs-canvas canvas {
width: 50%;
margin-left: auto;
margin-right: auto;
}

View file

@ -0,0 +1,349 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("katex"));
else if(typeof define === 'function' && define.amd)
define(["katex"], factory);
else if(typeof exports === 'object')
exports["renderMathInElement"] = factory(require("katex"));
else
root["renderMathInElement"] = factory(root["katex"]);
})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__771__) {
return /******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 771:
/***/ (function(module) {
module.exports = __WEBPACK_EXTERNAL_MODULE__771__;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function() { return module['default']; } :
/******/ function() { return module; };
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = function(exports, definition) {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!function() {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
"default": function() { return /* binding */ auto_render; }
});
// EXTERNAL MODULE: external "katex"
var external_katex_ = __webpack_require__(771);
var external_katex_default = /*#__PURE__*/__webpack_require__.n(external_katex_);
;// CONCATENATED MODULE: ./contrib/auto-render/splitAtDelimiters.js
/* eslint no-constant-condition:0 */
var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) {
// Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
var index = startIndex;
var braceLevel = 0;
var delimLength = delimiter.length;
while (index < text.length) {
var character = text[index];
if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) {
return index;
} else if (character === "\\") {
index++;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
}
index++;
}
return -1;
};
var escapeRegex = function escapeRegex(string) {
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
};
var amsRegex = /^\\begin{/;
var splitAtDelimiters = function splitAtDelimiters(text, delimiters) {
var index;
var data = [];
var regexLeft = new RegExp("(" + delimiters.map(function (x) {
return escapeRegex(x.left);
}).join("|") + ")");
while (true) {
index = text.search(regexLeft);
if (index === -1) {
break;
}
if (index > 0) {
data.push({
type: "text",
data: text.slice(0, index)
});
text = text.slice(index); // now text starts with delimiter
} // ... so this always succeeds:
var i = delimiters.findIndex(function (delim) {
return text.startsWith(delim.left);
});
index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length);
if (index === -1) {
break;
}
var rawData = text.slice(0, index + delimiters[i].right.length);
var math = amsRegex.test(rawData) ? rawData : text.slice(delimiters[i].left.length, index);
data.push({
type: "math",
data: math,
rawData: rawData,
display: delimiters[i].display
});
text = text.slice(index + delimiters[i].right.length);
}
if (text !== "") {
data.push({
type: "text",
data: text
});
}
return data;
};
/* harmony default export */ var auto_render_splitAtDelimiters = (splitAtDelimiters);
;// CONCATENATED MODULE: ./contrib/auto-render/auto-render.js
/* eslint no-console:0 */
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
* API, we should copy it before mutating.
*/
var renderMathInText = function renderMathInText(text, optionsCopy) {
var data = auto_render_splitAtDelimiters(text, optionsCopy.delimiters);
if (data.length === 1 && data[0].type === 'text') {
// There is no formula in the text.
// Let's return null which means there is no need to replace
// the current text node with a new one.
return null;
}
var fragment = document.createDocumentFragment();
for (var i = 0; i < data.length; i++) {
if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data));
} else {
var span = document.createElement("span");
var math = data[i].data; // Override any display mode defined in the settings with that
// defined by the text itself
optionsCopy.displayMode = data[i].display;
try {
if (optionsCopy.preProcess) {
math = optionsCopy.preProcess(math);
}
external_katex_default().render(math, span, optionsCopy);
} catch (e) {
if (!(e instanceof (external_katex_default()).ParseError)) {
throw e;
}
optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e);
fragment.appendChild(document.createTextNode(data[i].rawData));
continue;
}
fragment.appendChild(span);
}
}
return fragment;
};
var renderElem = function renderElem(elem, optionsCopy) {
for (var i = 0; i < elem.childNodes.length; i++) {
var childNode = elem.childNodes[i];
if (childNode.nodeType === 3) {
// Text node
// Concatenate all sibling text nodes.
// Webkit browsers split very large text nodes into smaller ones,
// so the delimiters may be split across different nodes.
var textContentConcat = childNode.textContent;
var sibling = childNode.nextSibling;
var nSiblings = 0;
while (sibling && sibling.nodeType === Node.TEXT_NODE) {
textContentConcat += sibling.textContent;
sibling = sibling.nextSibling;
nSiblings++;
}
var frag = renderMathInText(textContentConcat, optionsCopy);
if (frag) {
// Remove extra text nodes
for (var j = 0; j < nSiblings; j++) {
childNode.nextSibling.remove();
}
i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode);
} else {
// If the concatenated text does not contain math
// the siblings will not either
i += nSiblings;
}
} else if (childNode.nodeType === 1) {
(function () {
// Element node
var className = ' ' + childNode.className + ' ';
var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(function (x) {
return className.indexOf(' ' + x + ' ') === -1;
});
if (shouldRender) {
renderElem(childNode, optionsCopy);
}
})();
} // Otherwise, it's something else, and ignore it.
}
};
var renderMathInElement = function renderMathInElement(elem, options) {
if (!elem) {
throw new Error("No element provided to render");
}
var optionsCopy = {}; // Object.assign(optionsCopy, option)
for (var option in options) {
if (options.hasOwnProperty(option)) {
optionsCopy[option] = options[option];
}
} // default options
optionsCopy.delimiters = optionsCopy.delimiters || [{
left: "$$",
right: "$$",
display: true
}, {
left: "\\(",
right: "\\)",
display: false
}, // LaTeX uses $…$, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false},
// $ must come after $$
// Render AMS environments even if outside $$…$$ delimiters.
{
left: "\\begin{equation}",
right: "\\end{equation}",
display: true
}, {
left: "\\begin{align}",
right: "\\end{align}",
display: true
}, {
left: "\\begin{alignat}",
right: "\\end{alignat}",
display: true
}, {
left: "\\begin{gather}",
right: "\\end{gather}",
display: true
}, {
left: "\\begin{CD}",
right: "\\end{CD}",
display: true
}, {
left: "\\[",
right: "\\]",
display: true
}];
optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"];
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different
// math elements within a single call to `renderMathInElement`.
optionsCopy.macros = optionsCopy.macros || {};
renderElem(elem, optionsCopy);
};
/* harmony default export */ var auto_render = (renderMathInElement);
}();
__webpack_exports__ = __webpack_exports__["default"];
/******/ return __webpack_exports__;
/******/ })()
;
});

18819
themes/red/assets/js/katex.js Normal file

File diff suppressed because it is too large Load diff

51658
themes/red/assets/js/three.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,8 @@
{{ $path := .Get 0 }}
{{ $id := .Get 1 }}
{{ $content := readFile (print .Page.File.Dir $path) | safeHTML }}
<script type="x-shader/x-fragment" id="{{ $id }}">
{{ $content }}
</script>

View file

@ -0,0 +1,4 @@
{{ $path := .Get 0 }}
{{ $id := .Get 1 }}
<div class="threejs-canvas" id="{{ $id }}"></div>
<script type="module" src="{{ $path }}"></script>