<wc-geo-rt></wc-geo-rt>
const TWO_PI = Math.PI * 2
const QUARTER_TURN = Math.PI / 2;
function normalizeAngle(angle) {
if (angle < 0) {
return TWO_PI - (Math.abs(angle) % TWO_PI);
}
return angle % TWO_PI;
}
function radToDegrees(rad) {
return rad * 180 / Math.PI;
}
function cartesianToLatLng([x, y, z]) {
const radius = Math.sqrt(x ** 2 + y ** 2 + z ** 2);
return [
radius,
(Math.PI / 2) - Math.acos(y / radius),
normalizeAngle(Math.atan2(x, -z)),
];
}
function latLngToCartesian([radius, lat, lng]) {
lng = -lng + Math.PI / 2;
return [
radius * Math.cos(lat) * Math.cos(lng),
radius * Math.sin(lat),
radius * -Math.cos(lat) * Math.sin(lng),
];
}
function clamp(value, low, high) {
low = low !== undefined ? low : Number.MIN_SAFE_INTEGER;
high = high !== undefined ? high : Number.MAX_SAFE_INTEGER;
if (value < low) {
value = low;
}
if (value > high) {
value = high;
}
return value;
}
function lerp(start, end, normalValue) {
return start + (end - start) * normalValue;
}
function inverseLerp(start, end, value) {
return (value - start) / (end - start);
}
function normalizeNumber(num, len) {
num = parseFloat(num.toFixed(len));
num = num === -0 ? 0 : num;
return num;
}
function transpose(matrix) {
return [
[matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0]],
[matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1]],
[matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]],
[matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]]
];
}
function getRotationXMatrix(theta) {
return [
[1, 0, 0, 0],
[0, Math.cos(theta), -Math.sin(theta), 0],
[0, Math.sin(theta), Math.cos(theta), 0],
[0, 0, 0, 1]
];
}
function getRotationYMatrix(theta) {
return [
[Math.cos(theta), 0, Math.sin(theta), 0],
[0, 1, 0, 0],
[-Math.sin(theta), 0, Math.cos(theta), 0],
[0, 0, 0, 1]
];
}
function getRotationZMatrix(theta) {
return [
[Math.cos(theta), -Math.sin(theta), 0, 0],
[Math.sin(theta), Math.cos(theta), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
}
function getTranslationMatrix(x, y, z) {
return [
[1, 0, 0, x],
[0, 1, 0, y],
[0, 0, 1, z],
[0, 0, 0, 1]
];
}
function getScaleMatrix(x, y, z){
return [
[x, 0, 0, 0],
[0, y, 0, 0],
[0, 0, z, 0],
[0, 0, 0, 1]
];
}
function multiplyMatrix(a, b) {
const matrix = [
new Array(4),
new Array(4),
new Array(4),
new Array(4)
];
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 4; r++) {
matrix[r][c] = a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
}
}
return matrix;
}
function getIdentityMatrix() {
return [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
}
function multiplyMatrixVector(vector, matrix) {
//normalize 3 vectors
if (vector.length === 3) {
vector.push(1);
}
return [
vector[0] * matrix[0][0] + vector[1] * matrix[0][1] + vector[2] * matrix[0][2] + vector[3] * matrix[0][3],
vector[0] * matrix[1][0] + vector[1] * matrix[1][1] + vector[2] * matrix[1][2] + vector[3] * matrix[1][3],
vector[0] * matrix[2][0] + vector[1] * matrix[2][1] + vector[2] * matrix[2][2] + vector[3] * matrix[2][3],
vector[0] * matrix[3][0] + vector[1] * matrix[3][1] + vector[2] * matrix[3][2] + vector[3] * matrix[3][3]
];
}
function getVectorMagnitude(vec) {
let sum = 0;
for(const el of vec){
sum += el ** 2;
}
return Math.sqrt(sum);
}
function addVector(a, b) {
return [
a[0] + b[0],
a[1] + b[1],
a[2] + b[2]
];
}
function subtractVector(a, b) {
return [
a[0] - b[0],
a[1] - b[1],
a[2] - b[2]
];
}
function multiplyVector(vec, s) {
return [
vec[0] * s,
vec[1] * s,
vec[2] * s
];
}
function divideVector(vec, s) {
return [
vec[0] / s,
vec[1] / s,
vec[2] / s
];
}
function normalizeVector(vec) {
return divideVector(vec, getVectorMagnitude(vec));
}
function crossVector(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
function dotVector(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
function invertVector(vec){
return vec.map(x => -x);
}
const UP = [0, 1, 0];
const FORWARD = [0, 0, 1];
const RIGHT = [1, 0, 0];
///
function getVectorIntersectPlane(planePoint, planeNormal, lineStart, lineEnd) {
planeNormal = normalizeVector(planeNormal);
const planeDot = dotVector(planePoint, planeNormal);
const startDot = dotVector(lineStart, planeNormal);
const endDot = dotVector(lineEnd, planeNormal);
const t = (planeDot - startDot) / (endDot - startDot);
if (t === Infinity || t === -Infinity) {
return null;
}
const line = subtractVector(lineEnd, lineStart);
const deltaToIntersect = multiplyVector(line, t);
return addVector(lineStart, deltaToIntersect);
}
function isPointInInsideSpace(point, planeNormal, planePoint) {
planeNormal = normalizeVector(planeNormal);
return dotVector(planeNormal, subtractVector(planePoint, point)) > 0;
}
function reflectVector(vec, normal) {
return [
vec[0] - 2 * dotVector(vec, normal) * normal[0],
vec[1] - 2 * dotVector(vec, normal) * normal[1],
vec[2] - 2 * dotVector(vec, normal) * normal[2],
];
}
class Camera {
#position = [0, 0, -1];
#target = [0, 0, 0];
#screenWidth;
#screenHeight;
#near = 0.01;
#far = 5;
constructor(camera) {
this.#position = camera.position;
this.#screenWidth = camera.screenWidth;
this.#screenHeight = camera.screenHeight;
this.#near = camera.near ?? this.#near;
this.#far = camera.far ?? this.#far;
}
moveTo(x, y, z) {
this.#position = [x, y, z];
}
moveBy({ x = 0, y = 0, z = 0 }) {
this.#position[0] += x;
this.#position[1] += y;
this.#position[2] += z;
}
panBy({ x = 0, y = 0, z = 0 }) {
this.#position[0] += x;
this.#target[0] += x;
this.#position[1] += y;
this.#target[1] += y;
this.#position[2] += z;
this.#target[2] += z;
}
orbitBy({ lat = 0, long = 0, radius = 0 }) {
const [r, currentLat, currentLng] = this.getOrbit();
const newLat = clamp(currentLat + lat, -Math.PI / 2, Math.PI / 2);
const newRadius = Math.max(0.1, r + radius);
this.#position = latLngToCartesian([newRadius, newLat, currentLng - long]);
}
zoomBy(value) {
const [r, currentLat, currentLng] = this.getOrbit();
const newRadius = Math.max(0.1, r / value);
this.#position = latLngToCartesian([newRadius, currentLat, currentLng]);
}
lookAt(x, y, z) {
this.#target = [x, y, z];
}
getForwardDirection(){
return normalizeVector(subtractVector(this.#target, this.#position));
}
getRightDirection(){
return crossVector(UP, this.getForwardDirection());
}
getUpDirection(){
return crossVector(this.getForwardDirection(), this.getRightDirection());
}
getAspectRatio(){
return this.#screenWidth / this.#screenHeight;
}
getOrbit() {
const targetDelta = subtractVector(this.#position, this.#target);
return cartesianToLatLng(targetDelta);
}
getPosition() {
return this.#position;
}
setPosition(position) {
this.#position = position;
}
}
class WcGeoRt extends HTMLElement {
#context;
#width = 1280;
#height = 720;
constructor(){
super();
this.bind(this);
}
bind(element){
element.attachEvents = element.attachEvents.bind(element);
element.cacheDom = element.cacheDom.bind(element);
element.createShadowDom = element.createShadowDom.bind(element);
element.createCameras = element.createCameras.bind(element);
element.createMeshes = element.createMeshes.bind(element);
element.render = element.render.bind(element);
element.raytrace = element.raytrace.bind(element);
}
async connectedCallback() {
this.createShadowDom();
this.cacheDom();
this.attachEvents();
this.createCameras();
this.createMeshes();
this.#context = this.dom.canvas.getContext("2d");
this.render();
}
createShadowDom() {
this.shadow = this.attachShadow({ mode: "open" });
this.shadow.innerHTML = `
<style>
:host { display: block; }
</style>
<canvas width="${this.#width}" height="${this.#height}" style="border: 1px solid black"></canvas>
`;
}
createCameras(){
this.cameras = {
default: new Camera({
position: [0, 0, -2],
screenHeight: this.#height,
screenWidth: this.#width,
near: 0,
far: 5
})
}
}
createMeshes(){
this.meshes = {
sphere: {
position: [0,0,0],
radius: 1
}
}
}
cacheDom() {
this.dom = {
canvas: this.shadow.querySelector("canvas")
};
}
render(){
const pixelData = this.#context.getImageData(0, 0, this.#width, this.#height);
const halfVolumeHeight = 1;
const halfPixelHeight = this.#height / 2;
const pixelHeightRatio = halfVolumeHeight / halfPixelHeight;
const halfVolumeWidth = this.cameras.default.getAspectRatio();
const halfPixelWidth = this.#width / 2;
const pixelWidthRatio = halfVolumeWidth / halfPixelWidth;
for (let row = 0; row < this.#height; row++) {
for (let col = 0; col < this.#width; col++) {
const xDelta = multiplyVector(this.cameras.default.getRightDirection(), (col - halfPixelWidth) * pixelWidthRatio);
const yDelta = multiplyVector(multiplyVector(this.cameras.default.getUpDirection(), -1), (row - halfPixelHeight) * pixelHeightRatio);
const ray = {
origin: this.cameras.default.getPosition(),
direction: normalizeVector(addVector(addVector(this.cameras.default.getForwardDirection(), xDelta), yDelta))
};
const color = this.raytrace(ray);
const index = (row * this.#width * 4) + (col * 4);
pixelData.data[index + 0] = color[0];
pixelData.data[index + 1] = color[1];
pixelData.data[index + 2] = color[2];
pixelData.data[index + 3] = 255;
}
}
this.#context.putImageData(pixelData, 0, 0);
}
raytrace(ray){
const intersection = this.intersectObjects(ray);
if (intersection.distance === Infinity) {
return [255, 255, 255];
}
return [255, 0, 0];
}
intersectObjects(ray) {
let closest = { distance: Infinity, mesh: null };
for (let mesh of Object.values(this.meshes)) {
const distance = this.sphereIntersection(ray, mesh);
if (distance != undefined && distance < closest.distance) {
closest = { distance, mesh };
}
}
return closest;
}
sphereIntersection(ray, sphere) {
const a = dotVector(ray.direction, ray.direction);
const cameraToCenter = subtractVector(ray.origin, sphere.position);
const b = 2 * dotVector(ray.direction, cameraToCenter);
const c = dotVector(cameraToCenter, cameraToCenter) - sphere.radius ** 2;
const discriminant = (b ** 2) - (4 * a * c);
if(discriminant < 0) return undefined; //no solution, no hit
const s1 = (-b + Math.sqrt(discriminant)) / 2*a;
const s2 = (-b - Math.sqrt(discriminant)) / 2*a;
if(s1 < 0 || s2 < 0) return undefined; //either facing away or origin is inside sphere, no hit
return Math.min(s1, s2);
}
attachEvents() {
}
//Attrs
attributeChangedCallback(name, oldValue, newValue) {
if (newValue !== oldValue) {
this[name] = newValue;
}
}
set height(value) {
this.#height = value;
if (this.dom) {
this.dom.canvas.height = value;
}
}
set width(value) {
this.#width = value;
if (this.dom) {
this.dom.canvas.height = value;
}
}
}
customElements.define("wc-geo-rt", WcGeoRt);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.