You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

569 lines
14 KiB

2 years ago
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - gpgpu - protoplanet</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - <span id="protoplanets"></span> webgl gpgpu debris
</div>
<!-- Fragment shader for protoplanet's position -->
<script id="computeShaderPosition" type="x-shader/x-fragment">
#define delta ( 1.0 / 60.0 )
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
vec4 tmpPos = texture2D( texturePosition, uv );
vec3 pos = tmpPos.xyz;
vec4 tmpVel = texture2D( textureVelocity, uv );
vec3 vel = tmpVel.xyz;
float mass = tmpVel.w;
if ( mass == 0.0 ) {
vel = vec3( 0.0 );
}
// Dynamics
pos += vel * delta;
gl_FragColor = vec4( pos, 1.0 );
}
</script>
<!-- Fragment shader for protoplanet's velocity -->
<script id="computeShaderVelocity" type="x-shader/x-fragment">
// For PI declaration:
#include <common>
#define delta ( 1.0 / 60.0 )
uniform float gravityConstant;
uniform float density;
const float width = resolution.x;
const float height = resolution.y;
float radiusFromMass( float mass ) {
// Calculate radius of a sphere from mass and density
return pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
float idParticle = uv.y * resolution.x + uv.x;
vec4 tmpPos = texture2D( texturePosition, uv );
vec3 pos = tmpPos.xyz;
vec4 tmpVel = texture2D( textureVelocity, uv );
vec3 vel = tmpVel.xyz;
float mass = tmpVel.w;
if ( mass > 0.0 ) {
float radius = radiusFromMass( mass );
vec3 acceleration = vec3( 0.0 );
// Gravity interaction
for ( float y = 0.0; y < height; y++ ) {
for ( float x = 0.0; x < width; x++ ) {
vec2 secondParticleCoords = vec2( x + 0.5, y + 0.5 ) / resolution.xy;
vec3 pos2 = texture2D( texturePosition, secondParticleCoords ).xyz;
vec4 velTemp2 = texture2D( textureVelocity, secondParticleCoords );
vec3 vel2 = velTemp2.xyz;
float mass2 = velTemp2.w;
float idParticle2 = secondParticleCoords.y * resolution.x + secondParticleCoords.x;
if ( idParticle == idParticle2 ) {
continue;
}
if ( mass2 == 0.0 ) {
continue;
}
vec3 dPos = pos2 - pos;
float distance = length( dPos );
float radius2 = radiusFromMass( mass2 );
if ( distance == 0.0 ) {
continue;
}
// Checks collision
if ( distance < radius + radius2 ) {
if ( idParticle < idParticle2 ) {
// This particle is aggregated by the other
vel = ( vel * mass + vel2 * mass2 ) / ( mass + mass2 );
mass += mass2;
radius = radiusFromMass( mass );
}
else {
// This particle dies
mass = 0.0;
radius = 0.0;
vel = vec3( 0.0 );
break;
}
}
float distanceSq = distance * distance;
float gravityField = gravityConstant * mass2 / distanceSq;
gravityField = min( gravityField, 1000.0 );
acceleration += gravityField * normalize( dPos );
}
if ( mass == 0.0 ) {
break;
}
}
// Dynamics
vel += delta * acceleration;
}
gl_FragColor = vec4( vel, mass );
}
</script>
<!-- Particles vertex shader -->
<script type="x-shader/x-vertex" id="particleVertexShader">
// For PI declaration:
#include <common>
uniform sampler2D texturePosition;
uniform sampler2D textureVelocity;
uniform float cameraConstant;
uniform float density;
varying vec4 vColor;
float radiusFromMass( float mass ) {
// Calculate radius of a sphere from mass and density
return pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
}
void main() {
vec4 posTemp = texture2D( texturePosition, uv );
vec3 pos = posTemp.xyz;
vec4 velTemp = texture2D( textureVelocity, uv );
vec3 vel = velTemp.xyz;
float mass = velTemp.w;
vColor = vec4( 1.0, mass / 250.0, 0.0, 1.0 );
vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );
// Calculate radius of a sphere from mass and density
//float radius = pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
float radius = radiusFromMass( mass );
// Apparent size in pixels
if ( mass == 0.0 ) {
gl_PointSize = 0.0;
}
else {
gl_PointSize = radius * cameraConstant / ( - mvPosition.z );
}
gl_Position = projectionMatrix * mvPosition;
}
</script>
<!-- Particles fragment shader -->
<script type="x-shader/x-fragment" id="particleFragmentShader">
varying vec4 vColor;
void main() {
if ( vColor.y == 0.0 ) discard;
float f = length( gl_PointCoord - vec2( 0.5, 0.5 ) );
if ( f > 0.5 ) {
discard;
}
gl_FragColor = vColor;
}
</script>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js';
// Texture width for simulation (each texel is a debris particle)
const WIDTH = 64;
let container, stats;
let camera, scene, renderer, geometry;
const PARTICLES = WIDTH * WIDTH;
let gpuCompute;
let velocityVariable;
let positionVariable;
let velocityUniforms;
let particleUniforms;
let effectController;
init();
animate();
function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 5, 15000 );
camera.position.y = 120;
camera.position.z = 400;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild( renderer.domElement );
const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 100;
controls.maxDistance = 1000;
effectController = {
// Can be changed dynamically
gravityConstant: 100.0,
density: 0.45,
// Must restart simulation
radius: 300,
height: 8,
exponent: 0.4,
maxMass: 15.0,
velocity: 70,
velocityExponent: 0.2,
randVelocity: 0.001
};
initComputeRenderer();
stats = new Stats();
container.appendChild( stats.dom );
window.addEventListener( 'resize', onWindowResize );
initGUI();
initProtoplanets();
dynamicValuesChanger();
}
function initComputeRenderer() {
gpuCompute = new GPUComputationRenderer( WIDTH, WIDTH, renderer );
if ( renderer.capabilities.isWebGL2 === false ) {
gpuCompute.setDataType( THREE.HalfFloatType );
}
const dtPosition = gpuCompute.createTexture();
const dtVelocity = gpuCompute.createTexture();
fillTextures( dtPosition, dtVelocity );
velocityVariable = gpuCompute.addVariable( 'textureVelocity', document.getElementById( 'computeShaderVelocity' ).textContent, dtVelocity );
positionVariable = gpuCompute.addVariable( 'texturePosition', document.getElementById( 'computeShaderPosition' ).textContent, dtPosition );
gpuCompute.setVariableDependencies( velocityVariable, [ positionVariable, velocityVariable ] );
gpuCompute.setVariableDependencies( positionVariable, [ positionVariable, velocityVariable ] );
velocityUniforms = velocityVariable.material.uniforms;
velocityUniforms[ 'gravityConstant' ] = { value: 0.0 };
velocityUniforms[ 'density' ] = { value: 0.0 };
const error = gpuCompute.init();
if ( error !== null ) {
console.error( error );
}
}
function restartSimulation() {
const dtPosition = gpuCompute.createTexture();
const dtVelocity = gpuCompute.createTexture();
fillTextures( dtPosition, dtVelocity );
gpuCompute.renderTexture( dtPosition, positionVariable.renderTargets[ 0 ] );
gpuCompute.renderTexture( dtPosition, positionVariable.renderTargets[ 1 ] );
gpuCompute.renderTexture( dtVelocity, velocityVariable.renderTargets[ 0 ] );
gpuCompute.renderTexture( dtVelocity, velocityVariable.renderTargets[ 1 ] );
}
function initProtoplanets() {
geometry = new THREE.BufferGeometry();
const positions = new Float32Array( PARTICLES * 3 );
let p = 0;
for ( let i = 0; i < PARTICLES; i ++ ) {
positions[ p ++ ] = ( Math.random() * 2 - 1 ) * effectController.radius;
positions[ p ++ ] = 0; //( Math.random() * 2 - 1 ) * effectController.radius;
positions[ p ++ ] = ( Math.random() * 2 - 1 ) * effectController.radius;
}
const uvs = new Float32Array( PARTICLES * 2 );
p = 0;
for ( let j = 0; j < WIDTH; j ++ ) {
for ( let i = 0; i < WIDTH; i ++ ) {
uvs[ p ++ ] = i / ( WIDTH - 1 );
uvs[ p ++ ] = j / ( WIDTH - 1 );
}
}
geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
geometry.setAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );
particleUniforms = {
'texturePosition': { value: null },
'textureVelocity': { value: null },
'cameraConstant': { value: getCameraConstant( camera ) },
'density': { value: 0.0 }
};
// THREE.ShaderMaterial
const material = new THREE.ShaderMaterial( {
uniforms: particleUniforms,
vertexShader: document.getElementById( 'particleVertexShader' ).textContent,
fragmentShader: document.getElementById( 'particleFragmentShader' ).textContent
} );
material.extensions.drawBuffers = true;
const particles = new THREE.Points( geometry, material );
particles.matrixAutoUpdate = false;
particles.updateMatrix();
scene.add( particles );
}
function fillTextures( texturePosition, textureVelocity ) {
const posArray = texturePosition.image.data;
const velArray = textureVelocity.image.data;
const radius = effectController.radius;
const height = effectController.height;
const exponent = effectController.exponent;
const maxMass = effectController.maxMass * 1024 / PARTICLES;
const maxVel = effectController.velocity;
const velExponent = effectController.velocityExponent;
const randVel = effectController.randVelocity;
for ( let k = 0, kl = posArray.length; k < kl; k += 4 ) {
// Position
let x, z, rr;
do {
x = ( Math.random() * 2 - 1 );
z = ( Math.random() * 2 - 1 );
rr = x * x + z * z;
} while ( rr > 1 );
rr = Math.sqrt( rr );
const rExp = radius * Math.pow( rr, exponent );
// Velocity
const vel = maxVel * Math.pow( rr, velExponent );
const vx = vel * z + ( Math.random() * 2 - 1 ) * randVel;
const vy = ( Math.random() * 2 - 1 ) * randVel * 0.05;
const vz = - vel * x + ( Math.random() * 2 - 1 ) * randVel;
x *= rExp;
z *= rExp;
const y = ( Math.random() * 2 - 1 ) * height;
const mass = Math.random() * maxMass + 1;
// Fill in texture values
posArray[ k + 0 ] = x;
posArray[ k + 1 ] = y;
posArray[ k + 2 ] = z;
posArray[ k + 3 ] = 1;
velArray[ k + 0 ] = vx;
velArray[ k + 1 ] = vy;
velArray[ k + 2 ] = vz;
velArray[ k + 3 ] = mass;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
particleUniforms[ 'cameraConstant' ].value = getCameraConstant( camera );
}
function dynamicValuesChanger() {
velocityUniforms[ 'gravityConstant' ].value = effectController.gravityConstant;
velocityUniforms[ 'density' ].value = effectController.density;
particleUniforms[ 'density' ].value = effectController.density;
}
function initGUI() {
const gui = new GUI( { width: 280 } );
const folder1 = gui.addFolder( 'Dynamic parameters' );
folder1.add( effectController, 'gravityConstant', 0.0, 1000.0, 0.05 ).onChange( dynamicValuesChanger );
folder1.add( effectController, 'density', 0.0, 10.0, 0.001 ).onChange( dynamicValuesChanger );
const folder2 = gui.addFolder( 'Static parameters' );
folder2.add( effectController, 'radius', 10.0, 1000.0, 1.0 );
folder2.add( effectController, 'height', 0.0, 50.0, 0.01 );
folder2.add( effectController, 'exponent', 0.0, 2.0, 0.001 );
folder2.add( effectController, 'maxMass', 1.0, 50.0, 0.1 );
folder2.add( effectController, 'velocity', 0.0, 150.0, 0.1 );
folder2.add( effectController, 'velocityExponent', 0.0, 1.0, 0.01 );
folder2.add( effectController, 'randVelocity', 0.0, 50.0, 0.1 );
const buttonRestart = {
restartSimulation: function () {
restartSimulation();
}
};
folder2.add( buttonRestart, 'restartSimulation' );
folder1.open();
folder2.open();
}
function getCameraConstant( camera ) {
return window.innerHeight / ( Math.tan( THREE.MathUtils.DEG2RAD * 0.5 * camera.fov ) / camera.zoom );
}
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
gpuCompute.compute();
particleUniforms[ 'texturePosition' ].value = gpuCompute.getCurrentRenderTarget( positionVariable ).texture;
particleUniforms[ 'textureVelocity' ].value = gpuCompute.getCurrentRenderTarget( velocityVariable ).texture;
renderer.render( scene, camera );
}
</script>
</body>
</html>