webgl_gpgpu_protoplanet
对应 three.js 示例地址 。
仅需关注 init
函数的内容,其他部分都是示例小程序所使用的描述配置。
js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GPUComputationRenderer } from "three/examples/jsm/misc/GPUComputationRenderer.js";
/** @type {import("@minisheeep/mp-three-examples").OfficialExampleInfo} */
const exampleInfo = {
name: "webgl_gpgpu_protoplanet",
useLoaders: [],
info: [
[
{ tag: "a", link: "https://threejs.org", content: "three.js" },
{ tag: "text", content: "-" }
],
[{ tag: "text", content: "webgl gpgpu debris" }]
],
init: ({ window, canvas, GUI, Stats, needToDispose, useFrame }) => {
const WIDTH = 64;
let stats;
let camera, scene, renderer, geometry;
const PARTICLES = WIDTH * WIDTH;
let gpuCompute;
let velocityVariable;
let positionVariable;
let velocityUniforms;
let particleUniforms;
let effectController;
init();
function init() {
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 5, 15e3);
camera.position.y = 120;
camera.position.z = 400;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({ canvas });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 100;
controls.maxDistance = 1e3;
effectController = {
// Can be changed dynamically
gravityConstant: 100,
density: 0.45,
// Must restart simulation
radius: 300,
height: 8,
exponent: 0.4,
maxMass: 15,
velocity: 70,
velocityExponent: 0.2,
randVelocity: 1e-3
};
initComputeRenderer();
stats = new Stats(renderer);
window.addEventListener("resize", onWindowResize);
initGUI();
initProtoplanets();
dynamicValuesChanger();
needToDispose(renderer, scene, controls);
}
function initComputeRenderer() {
gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, renderer);
const dtPosition = gpuCompute.createTexture();
const dtVelocity = gpuCompute.createTexture();
fillTextures(dtPosition, dtVelocity);
velocityVariable = gpuCompute.addVariable(
"textureVelocity",
`
// 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 );
}
`,
dtVelocity
);
positionVariable = gpuCompute.addVariable(
"texturePosition",
`
#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 );
}
`,
dtPosition
);
gpuCompute.setVariableDependencies(velocityVariable, [positionVariable, velocityVariable]);
gpuCompute.setVariableDependencies(positionVariable, [positionVariable, velocityVariable]);
velocityUniforms = velocityVariable.material.uniforms;
velocityUniforms["gravityConstant"] = { value: 0 };
velocityUniforms["density"] = { value: 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;
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 }
};
const material = new THREE.ShaderMaterial({
uniforms: particleUniforms,
vertexShader: `
// 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;
}
`,
fragmentShader: `
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;
}
`
});
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) {
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);
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;
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, 1e3, 0.05).onChange(dynamicValuesChanger);
folder1.add(effectController, "density", 0, 10, 1e-3).onChange(dynamicValuesChanger);
const folder2 = gui.addFolder("Static parameters");
folder2.add(effectController, "radius", 10, 1e3, 1);
folder2.add(effectController, "height", 0, 50, 0.01);
folder2.add(effectController, "exponent", 0, 2, 1e-3);
folder2.add(effectController, "maxMass", 1, 50, 0.1);
folder2.add(effectController, "velocity", 0, 150, 0.1);
folder2.add(effectController, "velocityExponent", 0, 1, 0.01);
folder2.add(effectController, "randVelocity", 0, 50, 0.1);
const buttonRestart = {
restartSimulation: function() {
restartSimulation();
}
};
folder2.add(buttonRestart, "restartSimulation");
folder1.open();
folder2.open();
}
function getCameraConstant(camera2) {
return window.innerHeight / (Math.tan(THREE.MathUtils.DEG2RAD * 0.5 * camera2.fov) / camera2.zoom);
}
function 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);
}
}
};
export {
exampleInfo as default
};
ts
import { Loader, TypedArray } from 'three';
/**
* 官网示例的多端使用封装把版本
* */
export interface OfficialExampleInfo extends MiniProgramMeta {
/*** 示例名称(保持和官网一致)*/
name: string;
/** main */
init: (context: LoadContext) => void;
}
export interface LoadContext {
//为了减少官方代码的改动,实际上等同于 canvas
window: EventTarget & { innerWidth: number; innerHeight: number; devicePixelRatio: number };
/** HTMLCanvasElement */
canvas: any;
/** https://www.npmjs.com/package/lil-gui */
GUI: any;
/**
* https://www.npmjs.com/package/stats.js
* 也可以使用其他受支持的版本
* */
Stats: any;
/** 收集需要 dispose 的对象(官方示例没有处理这部分)*/
needToDispose: (...objs: any[]) => void | ((fromFn: () => any[]) => void);
/**基于 raq 的通用封装 */
useFrame(animateFunc: (/** ms */ delta: number) => void): { cancel: () => void };
/** 显示加载模态框 */
requestLoading(text?: string): Promise<void>;
/** 隐藏加载模态框*/
cancelLoading(): void;
/** 保存文件的通用封装*/
saveFile(
fileName: string,
data: ArrayBuffer | TypedArray | DataView | string
): Promise<string | null>;
/** 示例使用 DracoDecoder 时的资源路径 */
DecoderPath: {
GLTF: string;
STANDARD: string;
};
/** 为资源路径拼上 CDN 前缀 */
withCDNPrefix(path: string): string;
/**
* 在小程序中应使用 import { VideoTexture } from '@minisheep/three-platform-adapter/override/jsm/textures/VideoTexture.js';
* 正常情况(web) 可直接使用 THREE.VideoTexture
* */
getVideoTexture(videoOptions: VideoOptions): Promise<[{ isVideoTexture: true }, video: any]>;
/**
* 在小程序中应使用 import { CameraTexture } from '@minisheep/three-platform-adapter/override/jsm/textures/CameraTexture.js';
* 正常情况(web) 可参考示例 webgl_materials_video_webcam
* */
getCameraTexture(): { isVideoTexture: true };
/** 用于动态修改 info 中的占位符*/
bindInfoText(template: `$${string}$`, initValue?: string): { value: string };
/** 分屏控件对应的事件回调 */
onSlideStart(handle: () => void): void;
/** 分屏控件对应的事件回调 */
onSlideEnd(handle: () => void): void;
/** 分屏控件对应的事件回调 */
onSlideChange(handle: (offset: number, boxSize: number) => void): void;
}
export type VideoOptions = {
src: string;
/** 相当于 HTMLVideoElement 的 naturalWidth (小程序中获取不到)*/
width: number;
/** 相当于 HTMLVideoElement 的 naturalHeight (小程序中获取不到)*/
height: number;
loop?: boolean;
autoplay?: boolean;
muted?: boolean;
};
/** 示例小程序中使用的一些配置 */
export interface MiniProgramMeta {
/** 用于统计加载相关信息 */
useLoaders: Loader[];
/** 通用 info */
info: TagItem[][];
/** 特殊 info */
infoPanel?: {
left?: [string, string][];
right?: [string, string][];
};
/** 分屏控件配置 */
needSlider?: {
/** 方向 */
direction?: 'horizontal' | 'vertical';
/** 初始偏移 0-100 */
initPosition?: number;
};
/** 操作摇杆控件 */
needArrowControls?: boolean;
/** 默认需要的画布类型 */
canvasType?: '2d' | 'webgl' | 'webgl2';
/** 为保持效果一致所需要的画布样式 */
canvasStyle?: {
bgColor?: string;
width?: number | string;
height?: number | string;
};
/** 部分示例需要在加载前进行一些提示 */
initAfterConfirm?: {
/**
* 提示类型
* @default 'default'
* */
type?: 'warning' | 'default';
text: string[];
};
}
export interface BaseTag<T extends string> {
tag: T;
content: string;
}
export interface ATag extends BaseTag<'a'> {
link: string;
}
export type TextTag = BaseTag<'text'>;
export type TagItem = TextTag | ATag;