diff --git a/package-lock.json b/package-lock.json index dfc7b54..e863fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "three": "^0.147.0" }, "devDependencies": { + "@types/dat.gui": "^0.7.9", "@types/three": "^0.146.0", + "dat.gui": "^0.7.9", "typescript": "^4.6.4", "vite": "^3.2.3", "vite-plugin-plain-text": "^1.2.1" @@ -281,6 +283,12 @@ "node": ">= 10" } }, + "node_modules/@types/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-UiqZasQIask5cUwWOO6BgOjP1dNj9ChYtmeAb4WTKs5IJ7ha3ATRTeXY7/TzlmEZznh40lS6Ov5BdNA+tU+fWQ==", + "dev": true + }, "node_modules/@types/three": { "version": "0.146.0", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.146.0.tgz", @@ -296,6 +304,12 @@ "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==", "dev": true }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", + "dev": true + }, "node_modules/esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -1002,6 +1016,12 @@ "dev": true, "optional": true }, + "@types/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-UiqZasQIask5cUwWOO6BgOjP1dNj9ChYtmeAb4WTKs5IJ7ha3ATRTeXY7/TzlmEZznh40lS6Ov5BdNA+tU+fWQ==", + "dev": true + }, "@types/three": { "version": "0.146.0", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.146.0.tgz", @@ -1017,6 +1037,12 @@ "integrity": "sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==", "dev": true }, + "dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", + "dev": true + }, "esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", diff --git a/package.json b/package.json index c483510..74eff38 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "preview": "vite preview" }, "devDependencies": { + "@types/dat.gui": "^0.7.9", "@types/three": "^0.146.0", + "dat.gui": "^0.7.9", "typescript": "^4.6.4", "vite": "^3.2.3", "vite-plugin-plain-text": "^1.2.1" diff --git a/src/atmosphere.ts b/src/atmosphere.ts index 14fee5b..ce40599 100644 --- a/src/atmosphere.ts +++ b/src/atmosphere.ts @@ -5,6 +5,7 @@ import { Object3D, ShaderMaterial, SphereGeometry, + Vector3, } from 'three'; import { plainText as vertexShader } from './shaders/atmosphere.vert'; @@ -17,7 +18,7 @@ export class Atmosphere extends Object3D { public Scale = 1; public ScaleDepth = 0.25; public G = -0.95; - public Wavelength = [0.65, 0.57, 0.475]; + public Wavelength = new Vector3(0.65, 0.57, 0.475); public geom?: SphereGeometry; public mesh?: Mesh; @@ -35,12 +36,30 @@ export class Atmosphere extends Object3D { this.initialize(); } + set planetRadius(radius: number) { + const thickness = this.thickness; + this.innerRadius = radius; + this.outerRadius = radius + thickness; + this.refreshScale(); + } + get planetRadius() { + return this.innerRadius; + } + + set thickness(thickness: number) { + this.outerRadius = thickness + this.innerRadius; + this.refreshScale(); + } + get thickness() { + return this.outerRadius - this.innerRadius; + } + setUniforms(shader: ShaderMaterial) { shader.uniforms.invWavelength = { value: [ - 1 / Math.pow(this.Wavelength[0], 4), - 1 / Math.pow(this.Wavelength[1], 4), - 1 / Math.pow(this.Wavelength[2], 4), + 1 / Math.pow(this.Wavelength.x, 4), + 1 / Math.pow(this.Wavelength.y, 4), + 1 / Math.pow(this.Wavelength.z, 4), ], }; shader.uniforms.outerRadius = { value: this.outerRadius }; @@ -60,8 +79,18 @@ export class Atmosphere extends Object3D { } initialize() { + if (this.geom) { + this.geom.dispose(); + this.mesh && this.remove(this.mesh); + } this.geom = new SphereGeometry(this.outerRadius, 128, 128); this.mesh = new Mesh(this.geom, this.shader); this.add(this.mesh); } + + private refreshScale() { + this.Scale = 1 / (this.outerRadius - this.innerRadius); + this.setUniforms(this.shader); + this.initialize(); + } } diff --git a/src/globe.ts b/src/globe.ts index c8be1bd..2b4c274 100644 --- a/src/globe.ts +++ b/src/globe.ts @@ -7,6 +7,7 @@ import { Color, DirectionalLight, ShaderMaterial, + MeshPhongMaterial, } from 'three'; import { plainText as vertexShader } from './shaders/earth.vert'; @@ -14,15 +15,31 @@ import { plainText as fragmentShader } from './shaders/earth.frag'; import { Atmosphere } from './atmosphere'; export class Globe extends Object3D { - private sphere = new SphereGeometry(1, 128, 128); - private atmosphere = new Atmosphere(1, 1.05); - private earthMaterial?: ShaderMaterial; + private sphere?: SphereGeometry; + private clouds = new SphereGeometry(1.02, 64, 64); + public atmosphere = new Atmosphere(1, 1.05); + public earthMaterial?: ShaderMaterial; public earthMesh?: Mesh; + private loader = new TextureLoader(); + constructor(private planetRadius: number) { + super(); + } + + set radius(radius: number) { + this.planetRadius = radius; + this.atmosphere.planetRadius = radius; + this.earthMaterial && this.atmosphere.setUniforms(this.earthMaterial); + this.createMesh(); + } + get radius() { + return this.planetRadius; + } + public async loadTexture(name: string) { return new Promise((resolve, reject) => { - this.loader.load(`/${name}`, resolve, undefined, reject); + this.loader.load(`./${name}`, resolve, undefined, reject); }); } @@ -55,10 +72,19 @@ export class Globe extends Object3D { this.atmosphere.setUniforms(this.earthMaterial); this.atmosphere.setLight(light); - this.earthMesh = new Mesh(this.sphere, this.earthMaterial); - this.add(this.earthMesh); + this.createMesh(); this.add(this.atmosphere); } public tick() {} + + private createMesh() { + if (this.sphere) { + this.sphere.dispose(); + this.earthMesh && this.remove(this.earthMesh); + } + this.sphere = new SphereGeometry(this.planetRadius, 128, 128); + this.earthMesh = new Mesh(this.sphere, this.earthMaterial); + this.add(this.earthMesh); + } } diff --git a/src/main.ts b/src/main.ts index ac92996..f94b593 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { Renderer } from './renderer'; import { DirectionalLight } from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import './style.css'; +import { GUI } from 'dat.gui'; const main = new Renderer(); @@ -10,7 +11,7 @@ const dirLight = new DirectionalLight(0xffffff, 1); dirLight.position.set(10, 10, 10); main.scene.add(dirLight); -const globe = new Globe(); +const globe = new Globe(1); main.resize(); globe.initialize(dirLight).catch(console.error); main.scene.add(globe); @@ -19,6 +20,58 @@ main.camera.position.set(0, 0, 2); const control = new OrbitControls(main.camera, main.canvas); +const gui = new GUI(); +const atmosGui = gui.addFolder('Atmosphere'); +const updateAtmosShader = () => { + globe.atmosphere.setUniforms(globe.atmosphere.shader); + globe.atmosphere.setUniforms(globe.earthMaterial!); +}; +// any's because for some reason the generic expects a string indexed record +atmosGui + .add(globe.atmosphere.Wavelength, 'x', 0, 1) + .name('R') + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere.Wavelength, 'y', 0, 1) + .name('G') + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere.Wavelength, 'z', 0, 1) + .name('B') + .onChange(updateAtmosShader); + +atmosGui + .add(globe.atmosphere, 'thickness', 0.001, 0.5) + .name('Thickness') + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere, 'ScaleDepth', 0.01, 1) + .name('Scale depth') + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere, 'Rayleigh', 0.0001, 0.01) + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere, 'Mie', 0.0001, 0.01) + .onChange(updateAtmosShader); +atmosGui + .add(globe.atmosphere, 'Exposure', 0, 100) + .onChange(updateAtmosShader); +atmosGui.open(); + +const planetGui = gui.addFolder('Planet'); +planetGui + .add(globe.rotation, 'x', -Math.PI * 2, Math.PI * 2, 0.01) + .name('Rotation X'); +planetGui + .add(globe.rotation, 'y', -Math.PI * 2, Math.PI * 2, 0.01) + .name('Rotation Y'); +planetGui + .add(globe.rotation, 'z', -Math.PI * 2, Math.PI * 2, 0.01) + .name('Rotation Z'); +// planetGui.add(globe, 'radius', 1, 100).name('Radius'); +planetGui.open(); + function tick() { requestAnimationFrame(tick); control.update(); diff --git a/src/shaders/earth.vert b/src/shaders/earth.vert index 384cf90..c07de86 100644 --- a/src/shaders/earth.vert +++ b/src/shaders/earth.vert @@ -1,10 +1,16 @@ #define EARTH +#include +#include + varying vec3 vViewPosition; varying vec3 vLightDirection; varying vec2 vUv; varying vec3 vEyeNormal; -#include -#include + +varying vec3 v3Direction; +varying vec3 c0; +varying vec3 c1; + uniform vec3 light; uniform vec3 invWavelength; // 1 / pow(wavelength, 4) for the red, green, and blue channels @@ -19,10 +25,6 @@ uniform float scaleDepth; // The scale depth (i.e. the altitude at which the const int nSamples = 2; const float fSamples = 1.0; -varying vec3 v3Direction; -varying vec3 c0; -varying vec3 c1; - float dscale(float fCos) { float x = 1.0 - fCos; return scaleDepth * exp(-0.00287 + x * (0.459 + x * (3.83 + x * (-6.80 + x * 5.25)))); diff --git a/vite.config.js b/vite.config.js index 05e77b4..3593df8 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,4 +3,5 @@ import plainText from 'vite-plugin-plain-text'; export default defineConfig({ plugins: [plainText.default(/\.vert|\.frag$/)], + base: '', });