Add earth widget
1
.gitignore
vendored
@ -2,6 +2,7 @@ _site
|
||||
.sass-cache
|
||||
.jekyll-cache
|
||||
.jekyll-metadata
|
||||
.jampack/
|
||||
.obsidian/
|
||||
Gemfile.lock
|
||||
vendor
|
||||
|
@ -9,4 +9,12 @@ excerpt: |
|
||||
|
||||
The site is build by a github action based on the [jekyll recommendation](https://jekyllrb.com/docs/continuous-integration/github-actions/). I did this because I wanted to be able to use `npm install` like all the cool kids which lead to adding [jekyll-node-module](https://github.com/mintbit/jekyll-node-module#jekyll-node-module) to the site to be able to copy things in from `node_packages` without committing it all to the repo.
|
||||
|
||||
The 3D outline rendered images on the [projects pages](/projects) are done with code from [Omaha Shehata](https://omar-shehata.medium.com/better-outline-rendering-using-surface-ids-with-webgl-e13cdab1fd94).
|
||||
The 3D outline rendered images on the [projects pages](/projects) are done with code from [Omaha Shehata](https://omar-shehata.medium.com/better-outline-rendering-using-surface-ids-with-webgl-e13cdab1fd94).
|
||||
|
||||
## Future
|
||||
There are a few plugins I'm thinking of adding:
|
||||
- [jekyll-auto-image](https://github.com/merlos/jekyll-auto-image) to simply choosing a representative image for a particular post.
|
||||
- I'd like to write a jekyll plugin for [D2](https://github.com/terrastruct/d2) diagrams. It doesn't [look too difficult](https://jekyllrb.com/docs/plugins/tags/).
|
||||
- I'm wondering if I should switch the rendering to use pandoc, I used pandoc in the past to convert my thesis to multiple output formats and I suspect it is now a more active project than jekyll itself, for example someone has already written a D2 pandoc filter.
|
||||
|
||||
There's a good list of jekyll plugins [here](https://github.com/planetjekyll/awesome-jekyll-plugins).
|
@ -10,6 +10,7 @@ assets: /assets/blog/bookshelf/
|
||||
alt:
|
||||
head: <script type="module" src="/assets/js/model-viewer.js"></script>
|
||||
---
|
||||
|
||||
Here's a little weekend project. We have this mezzanine bed with windows and window shelves around it. I'd like to put books there but sometimes condensation pools so I need something to raise the books a little above the level of the shelf.
|
||||
|
||||
<figure>
|
||||
|
26
_posts/2024-05-17-threejs-earth.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: ThreeJS Earth
|
||||
layout: post
|
||||
excerpt: A small earth renderer made using ThreeJS
|
||||
|
||||
image:
|
||||
thumbnail:
|
||||
assets:
|
||||
alt:
|
||||
|
||||
head: |
|
||||
<script async src="/node_modules/es-module-shims/dist/es-module-shims.js"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/node_modules/three/build/three.module.min.js",
|
||||
"three/addons/": "/node_modules/three/examples/jsm/",
|
||||
"three/examples/": "/node_modules/three/examples/jsm/",
|
||||
"lil-gui": "/node_modules/lil-gui/dist/lil-gui.esm.min.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="/assets/blog/sphere_geodesics/index.js" type="module"></script>
|
||||
---
|
||||
A small earth renderer made using ThreeJS.
|
||||
<sphere-viewer style="height: 500px; display: block;"></sphere-viewer>
|
BIN
assets/blog/sphere_geodesics/earthbump10k.jpg
Normal file
After Width: | Height: | Size: 5.6 MiB |
BIN
assets/blog/sphere_geodesics/earthbump1k.jpg
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
assets/blog/sphere_geodesics/earthcloudmap.jpg
Normal file
After Width: | Height: | Size: 230 KiB |
BIN
assets/blog/sphere_geodesics/earthcloudmaptrans.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
assets/blog/sphere_geodesics/earthlights10k.jpg
Normal file
After Width: | Height: | Size: 4.8 MiB |
BIN
assets/blog/sphere_geodesics/earthlights1k.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
assets/blog/sphere_geodesics/earthmap10k.jpg
Normal file
After Width: | Height: | Size: 11 MiB |
BIN
assets/blog/sphere_geodesics/earthmap1k.jpg
Normal file
After Width: | Height: | Size: 336 KiB |
BIN
assets/blog/sphere_geodesics/earthspec10k.jpg
Normal file
After Width: | Height: | Size: 3.2 MiB |
BIN
assets/blog/sphere_geodesics/earthspec1k.jpg
Normal file
After Width: | Height: | Size: 114 KiB |
254
assets/blog/sphere_geodesics/index.js
Normal file
@ -0,0 +1,254 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import GUI from 'lil-gui'
|
||||
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { TransformControls } from 'three/addons/controls/TransformControls.js';
|
||||
import Stats from "three/examples/libs/stats.module.js";
|
||||
// node_modules/three/examples/jsm/libs/stats.module.js
|
||||
|
||||
// previously this feature is .legacyMode = false, see https://www.donmccurdy.com/2020/06/17/color-management-in-threejs/
|
||||
// turning this on has the benefit of doing certain automatic conversions (for hexadecimal and CSS colors from sRGB to linear-sRGB)
|
||||
THREE.ColorManagement.enabled = true
|
||||
|
||||
class SphereViewer extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
let component_rect = this.getBoundingClientRect();
|
||||
console.log("component_rect", component_rect);
|
||||
|
||||
this.shadow = this.attachShadow({ mode: "open" });
|
||||
this.render(component_rect.height);
|
||||
|
||||
const container = this.shadow.querySelector("div#container");
|
||||
const canvas = this.shadow.querySelector("canvas");
|
||||
|
||||
let canvas_rect = canvas.getBoundingClientRect();
|
||||
|
||||
// determine the outline and bg colors
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
const style = window.getComputedStyle(body);
|
||||
const outline_color = style.getPropertyValue("--theme-model-line-color");
|
||||
const model_color = style.getPropertyValue("--theme-model-bg-color");
|
||||
|
||||
// // Init scene
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color( model_color);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera( 70, canvas_rect.width / canvas_rect.height, 0.01, 1000 );
|
||||
camera.position.set( 0, 200, 200 );
|
||||
camera.lookAt(0,0,0);
|
||||
scene.add( camera );
|
||||
|
||||
scene.add( new THREE.AmbientLight( 0xf0f0f0, 3 ) );
|
||||
const light = new THREE.SpotLight( 0xffffff, 4.5 );
|
||||
light.position.set( 0, 1500, 200 );
|
||||
light.angle = Math.PI * 0.2;
|
||||
light.decay = 0;
|
||||
light.castShadow = true;
|
||||
light.shadow.camera.near = 200;
|
||||
light.shadow.camera.far = 2000;
|
||||
light.shadow.bias = - 0.000222;
|
||||
light.shadow.mapSize.width = 1024;
|
||||
light.shadow.mapSize.height = 1024;
|
||||
scene.add( light );
|
||||
|
||||
// Texture loading
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
function load_texture(url) {
|
||||
const texture = loader.load(url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Sphere
|
||||
const geometry = new THREE.SphereGeometry( 100, 1000, 1000 );
|
||||
const earth_materials = {
|
||||
earth1k: new THREE.MeshPhongMaterial({
|
||||
color: 0xffffff,
|
||||
map: load_texture("/assets/blog/sphere_geodesics/earthmap1k.jpg"),
|
||||
bumpMap: load_texture("/assets/blog/sphere_geodesics/earthbump1k.jpg"),
|
||||
displacementMap: load_texture("/assets/blog/sphere_geodesics/earthbump1k.jpg"),
|
||||
emissiveMap: load_texture("/assets/blog/sphere_geodesics/earthlights1k.jpg"),
|
||||
specularMap: load_texture("/assets/blog/sphere_geodesics/earthspec1k.jpg"),
|
||||
emissive: 0xffffff,
|
||||
}),
|
||||
earth10k: new THREE.MeshPhongMaterial({
|
||||
color: 0xffffff,
|
||||
map: load_texture("/assets/blog/sphere_geodesics/earthmap10k.jpg"),
|
||||
bumpMap: load_texture("/assets/blog/sphere_geodesics/earthbump10k.jpg"),
|
||||
displacementMap: load_texture("/assets/blog/sphere_geodesics/earthbump10k.jpg"),
|
||||
emissiveMap: load_texture("/assets/blog/sphere_geodesics/earthlights10k.jpg"),
|
||||
specularMap: load_texture("/assets/blog/sphere_geodesics/earthspec10k.jpg"),
|
||||
emissive: 0xffffff,
|
||||
}),
|
||||
checkerboard: new THREE.MeshBasicMaterial({
|
||||
onBeforeCompile: shader => {
|
||||
shader.fragmentShader = `${shader.fragmentShader}`
|
||||
.replace(
|
||||
`vec4 diffuseColor = vec4( diffuse, opacity );`,
|
||||
`
|
||||
float chCount = 15.;
|
||||
float checker = (1. / chCount);
|
||||
float actualCheckers = 1. - checker;
|
||||
float halfChecker = checker * 0.5;
|
||||
vec2 bUv = (vUv * actualCheckers) - halfChecker;
|
||||
vec2 cUv = fract((bUv) * (chCount * 0.5)) - 0.5;
|
||||
float checkerVal = clamp(step(cUv.x * cUv.y, 0.), 0.5, 1.);
|
||||
vec3 col = vec3(checkerVal);
|
||||
vec4 diffuseColor = vec4( col, opacity );
|
||||
`
|
||||
);},
|
||||
opacity: 0.9,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
})
|
||||
};
|
||||
|
||||
earth_materials["checkerboard"].defines = {"USE_UV":""};
|
||||
|
||||
const sphere = new THREE.Mesh(geometry, earth_materials["earth1k"]);
|
||||
scene.add( sphere );
|
||||
|
||||
|
||||
// const planeGeometry = new THREE.PlaneGeometry( 2000, 2000 );
|
||||
// planeGeometry.rotateX( - Math.PI / 2 );
|
||||
// const planeMaterial = new THREE.ShadowMaterial( { color: 0x000000, opacity: 0.2 } );
|
||||
|
||||
// const plane = new THREE.Mesh( planeGeometry, planeMaterial );
|
||||
// plane.position.y = - 200;
|
||||
// plane.receiveShadow = true;
|
||||
// scene.add( plane );
|
||||
|
||||
// const helper = new THREE.GridHelper( 2000, 100 );
|
||||
// helper.position.y = - 199;
|
||||
// helper.material.opacity = 0.25;
|
||||
// helper.material.transparent = true;
|
||||
// scene.add( helper );
|
||||
|
||||
const renderer = new THREE.WebGLRenderer( {canvas: canvas, antialias: true},
|
||||
(_renderer) => {
|
||||
// best practice: ensure output colorspace is in sRGB, see Color Management documentation:
|
||||
// https://threejs.org/docs/#manual/en/introduction/Color-management
|
||||
_renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
})
|
||||
renderer.setPixelRatio( window.devicePixelRatio );
|
||||
renderer.setSize(canvas_rect.width, canvas_rect.height, false);
|
||||
renderer.shadowMap.enabled = true;
|
||||
|
||||
// Controls
|
||||
const controls = new OrbitControls(camera, canvas);
|
||||
controls.damping = 0.2;
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 2;
|
||||
controls.target.set(0, 0, 0);
|
||||
|
||||
// If not using render loop, render on changes
|
||||
// controls.addEventListener( 'change', render);
|
||||
// window.addEventListener('resize', render);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
// Render single frame
|
||||
function render() {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Render loop
|
||||
function loop() {
|
||||
requestAnimationFrame(loop);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
loop();
|
||||
|
||||
const gui = new GUI({
|
||||
title: "Settings",
|
||||
container: container,
|
||||
injectStyles: false,
|
||||
closeFolders: true,
|
||||
});
|
||||
const params = {
|
||||
Material: "earth1k",
|
||||
rotate: true,
|
||||
emissiveIntensity: 0,
|
||||
emissiveColor: 0xffcd75,
|
||||
specularColor: 0xfbf9c1,
|
||||
displacementScale: 1,
|
||||
bumpScale: 1,
|
||||
}
|
||||
gui.add(params, "Material", earth_materials)
|
||||
.onChange(material => {sphere.material = material});
|
||||
|
||||
gui.add(params, 'rotate')
|
||||
.onChange(value => {controls.autoRotate = value});
|
||||
|
||||
gui.add(params, 'emissiveIntensity', 0, 10)
|
||||
.onChange(value => {sphere.material.emissiveIntensity = value});
|
||||
|
||||
gui.add(params, 'displacementScale', 0, 10)
|
||||
.onChange(value => {sphere.material.displacementScale = value});
|
||||
|
||||
gui.add(params, 'bumpScale', 0, 10)
|
||||
.onChange(value => {sphere.material.bumpScale = value});
|
||||
|
||||
gui.addColor(params, 'emissiveColor')
|
||||
.onChange(color => {sphere.material.emissive = new THREE.Color(color)});
|
||||
|
||||
gui.addColor(params, 'specularColor')
|
||||
.onChange(color => {sphere.material.specular = new THREE.Color(color)});
|
||||
|
||||
|
||||
}
|
||||
|
||||
render(height) {
|
||||
this.shadow.innerHTML = `
|
||||
<div id="container">
|
||||
<canvas class = "object-viewer"></canvas>
|
||||
</div>
|
||||
<link rel="stylesheet" href="/node_modules/lil-gui/dist/lil-gui.min.css">
|
||||
<style>
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.lil-gui .title {height: 2em;}
|
||||
.lil-gui.root {
|
||||
margin-top: calc(${height}px - 2em);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
--background-color: none;
|
||||
--text-color: var(--theme-text-color);
|
||||
--title-background-color: none;
|
||||
--title-text-color: var(--theme-text-color);
|
||||
--widget-color: var(--theme-subtle-outline);
|
||||
--hover-color: lightgrey;
|
||||
--focus-color: lightgrey;
|
||||
--number-color: #2cc9ff;
|
||||
--string-color: #a2db3c;
|
||||
}
|
||||
|
||||
.lil-gui button {
|
||||
border: var(--theme-subtle-outline) 1px solid;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: ${height}px;
|
||||
border-radius: inherit;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("sphere-viewer", SphereViewer);
|