diff --git a/demos/data/cube-map-nx.jpg b/demos/data/cube-map-nx.jpg new file mode 100644 index 00000000..5b61322a Binary files /dev/null and b/demos/data/cube-map-nx.jpg differ diff --git a/demos/data/cube-map-ny.jpg b/demos/data/cube-map-ny.jpg new file mode 100644 index 00000000..a7761ea2 Binary files /dev/null and b/demos/data/cube-map-ny.jpg differ diff --git a/demos/data/cube-map-nz.jpg b/demos/data/cube-map-nz.jpg new file mode 100644 index 00000000..e9d72f7e Binary files /dev/null and b/demos/data/cube-map-nz.jpg differ diff --git a/demos/data/cube-map-px.jpg b/demos/data/cube-map-px.jpg new file mode 100644 index 00000000..92fb4a90 Binary files /dev/null and b/demos/data/cube-map-px.jpg differ diff --git a/demos/data/cube-map-py.jpg b/demos/data/cube-map-py.jpg new file mode 100644 index 00000000..9b2f59b8 Binary files /dev/null and b/demos/data/cube-map-py.jpg differ diff --git a/demos/data/cube-map-pz.jpg b/demos/data/cube-map-pz.jpg new file mode 100644 index 00000000..83478fc6 Binary files /dev/null and b/demos/data/cube-map-pz.jpg differ diff --git a/demos/demos.json b/demos/demos.json index 5910a15d..0878e73d 100644 --- a/demos/demos.json +++ b/demos/demos.json @@ -73,11 +73,11 @@ "target": "metaballs", "title": "Metaballs", "authors": [ - "cgcostume", - "bwasty" + "MartinBuessemeyer", + "cgcostume" ], "caption": "Raymarched Metaballs with 3D force field, reflections, and some post processing.", - "disabled": true + "disabled": false }, { "target": "cubescape", diff --git a/demos/metaballs/metaballs.frag b/demos/metaballs/metaballs.frag new file mode 100644 index 00000000..bf10e249 --- /dev/null +++ b/demos/metaballs/metaballs.frag @@ -0,0 +1,274 @@ +precision lowp float; + +@import ../../source/shaders/facade.frag; + + +#if __VERSION__ == 100 + #define fragColor gl_FragColor +#else + layout(location = 0) out vec4 fragColor; +#endif + +uniform sampler2D u_metaballsTexture; +uniform sampler2D u_metaballColorsTexture; +uniform int u_metaballsTextureSize; +uniform int u_metaballsColorTextureSize; + +uniform sampler2D u_lightsTexture; +uniform mat4 u_inverseViewProjection; +uniform int u_lightsTextureSize; + +uniform samplerCube u_cubemap; + +varying vec2 v_uv; +varying vec4 v_ray; +varying vec4 v_origin; + + +# define BASE_ENERGY 0.06 +# define THRESHOLD 0.50 +# define METABALL_TRANSPARENCY 0.8 +# define RAYMARCH_DISTANCE_FACTOR 6.0 +# define METABALL_BUFFER_SIZE 16 + +// NOTE: the phong shading coeffcients are currently assumed to be the same for all metaballs. +# define AMBIENT_ILLUMINATION 0.5 +# define DIFFUSE_ILLUMINATION 0.7 +# define SPECULAR_ILLUMINATION 0.7 +# define REFRACTION_INDEX 0.96 +# define REFRACTION_AND_REFLECTION_INTENSITY 0.80 + +vec4 metaballArray[METABALL_BUFFER_SIZE]; +vec3 metaballColorArray[METABALL_BUFFER_SIZE]; + +struct FragmentValues { + float energy; + vec4 normal; + vec3 color; + vec4 rayPosition; + float marchDistance; +}; + +vec2 calculateAbsoluteTextureCoords(int width, int height, int maxWidth, int maxHeight) { + return vec2( + (2.0 * float(width) + 1.0) / (2.0 * float(maxWidth)), + (2.0 * float(height) + 1.0) / (2.0 * float(maxHeight)) + ); +} + +float distFunc(vec4 metaball, vec4 rayPosition) { + return metaball.w / distance(metaball.xyz, rayPosition.xyz); +} + +float fresnelReflection(vec3 rayDirection, vec3 normal) +{ + float cosi = clamp(dot(rayDirection, normal), -1.0, 1.0); + float etai = cosi > 0.0 ? REFRACTION_INDEX : 1.0; + float etat = cosi > 0.0 ? 1.0 : REFRACTION_INDEX; + float sint = etai / etat * sqrt(max(0.0, 1.0 - cosi * cosi)); + return cosi + 0.7; + if (sint >= 1.0) { + return 1.0; + } + else + { + float cost = sqrt(max(0.0, 1.0 - sint * sint)); + cosi = abs(cosi); + float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost)); + float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost)); + return (Rs * Rs + Rp * Rp) / 2.0; + } +} + +vec4 getMetaballPositionAnEnergy(int index){ + //return metaballArray[index]; + vec4 metaball = texelFetch(u_metaballsTexture, ivec2(index * 2, 0), 0); + metaball.w *= BASE_ENERGY; + return metaball; +} + +vec3 getMetaballColor(int index){ + //return metaballColorArray[index]; + return texelFetch(u_metaballColorsTexture, ivec2(index, 0), 0).xyz; +} + +float calculateEnergy(vec4 currentRayPosition) { + float energy = 0.0; + for (int metaballIndex = 0; metaballIndex < u_metaballsTextureSize; metaballIndex++) { + vec4 currentMetaball = getMetaballPositionAnEnergy(metaballIndex); + energy += distFunc(currentMetaball, currentRayPosition); + } + return energy; +} + +void calculateEnergyAndOtherFragmentValues(vec4 currentRayPosition, inout FragmentValues fragmentValues) { + fragmentValues.normal = vec4(0.0); + fragmentValues.energy = 0.0; + for (int metaballIndex = 0; metaballIndex < u_metaballsTextureSize; metaballIndex++) { + + vec4 currentMetaball = getMetaballPositionAnEnergy(metaballIndex); + float currentEnergy = distFunc(currentMetaball, currentRayPosition); + fragmentValues.energy += currentEnergy; + vec4 currentNormal = normalize(currentRayPosition - vec4(currentMetaball.xyz, 1.0)); + fragmentValues.normal += currentNormal * (currentEnergy * 50.0); + fragmentValues.color += getMetaballColor(metaballIndex) * currentEnergy; + } + fragmentValues.rayPosition = currentRayPosition; + fragmentValues.normal = normalize(fragmentValues.normal); +} + +float calculateEnergyOverDerivative(vec4 rayPosition, vec4 rayDirection, float marchDistance, bool isInverseRayMarch) { + float derivativeDistance = isInverseRayMarch ? -0.1 : 0.1; + float energy = 0.0; + float nearerEnergy = 0.0; + vec4 currentRayPosition = rayPosition + rayDirection * marchDistance; + vec4 currentNearerRayPosition = rayPosition + rayDirection * (marchDistance - derivativeDistance); + for (int metaballIndex = 0; metaballIndex < u_metaballsTextureSize; metaballIndex++) { + vec4 currentMetaball = metaballArray[metaballIndex]; + energy += distFunc(currentMetaball, currentRayPosition); + nearerEnergy += distFunc(currentMetaball, currentNearerRayPosition); + } + float derivativeValue = (energy - nearerEnergy) / (derivativeDistance); + derivativeValue = derivativeValue != 0.0 ? derivativeValue : 0.0000000001; + return (energy - THRESHOLD) / derivativeValue; +} + +void newtonMethod(vec4 rayPosition, vec4 rayDirection, float marchDistance, int numberOfNewtonIterations, bool isInverseNewton, inout FragmentValues fragmentValues) +{ + for (int i = 0; i < numberOfNewtonIterations; i++) + { + float energyOverDerivative = calculateEnergyOverDerivative(rayPosition, rayDirection, marchDistance, false); + marchDistance = marchDistance - energyOverDerivative; + } + vec4 finalRayPositionAndEnergy = rayPosition + rayDirection * marchDistance; + calculateEnergyAndOtherFragmentValues(finalRayPositionAndEnergy, fragmentValues); + // Set the energy slightly higher since we might be slightly below the threshold because of the newton method. + fragmentValues.energy = THRESHOLD + 0.01; +} + +void rayMarchWithFragmentValues(vec4 rayPosition, vec4 rayDirection, float stepWidth, float initialMarchDistance, int numberOfNewtonIterations, bool isInverseMarch, bool exitedMetaBall, inout FragmentValues fragmentValues) { + for (float marchDistance = initialMarchDistance; marchDistance < RAYMARCH_DISTANCE_FACTOR; marchDistance += stepWidth) { + vec4 currentRayPosition = rayPosition + rayDirection * marchDistance; + float energy = calculateEnergy(currentRayPosition); + if ((energy > THRESHOLD && !isInverseMarch || energy < THRESHOLD && isInverseMarch) && exitedMetaBall) { + newtonMethod(rayPosition, rayDirection, marchDistance, numberOfNewtonIterations, isInverseMarch, fragmentValues); + fragmentValues.marchDistance = marchDistance; + return; + } + exitedMetaBall = exitedMetaBall || energy < THRESHOLD || marchDistance > 0.2; + } +} + +bool rayMarchHitMetaball(vec4 rayPosition, vec4 rayDirection, float stepWidth, float initialMarchDistance) { + FragmentValues fragmentValues; + for (float marchDistance = initialMarchDistance; marchDistance < RAYMARCH_DISTANCE_FACTOR; marchDistance += stepWidth) { + vec4 currentRayPosition = rayPosition + rayDirection * marchDistance; + float energy = calculateEnergy(currentRayPosition); + if (energy > THRESHOLD) { + return true; + } + } + return false; +} + +float calculateIllumination(FragmentValues fragmentValues) { + // Phong shading + float illumination = AMBIENT_ILLUMINATION; + for (int lightIndex = 0; lightIndex < u_lightsTextureSize; lightIndex++) { + vec4 pointLightPositionAndShininess = texelFetch(u_lightsTexture, ivec2(lightIndex, 0), 0); + vec4 lightDirection = vec4(pointLightPositionAndShininess.xyz, 1.0) - fragmentValues.rayPosition; + bool hitMetaballBetweenLight = rayMarchHitMetaball(fragmentValues.rayPosition, lightDirection, 0.1, 0.11); + if (!hitMetaballBetweenLight) { + vec4 lightDirectionNormalized = normalize(lightDirection); + vec4 reflectDir = reflect(-lightDirectionNormalized, fragmentValues.normal); + + float diffuse = max(dot(fragmentValues.normal, lightDirectionNormalized), 0.0); + + float specularAngle = max(dot(reflectDir, fragmentValues.normal), 0.0); + float specular = pow(specularAngle, pointLightPositionAndShininess.w); + + illumination += DIFFUSE_ILLUMINATION * diffuse; + illumination += SPECULAR_ILLUMINATION * specular; + } + } + return illumination; +} + +float fullRayMarchWithLightCalculation(vec4 rayPosition, vec4 rayDirection, bool isInverseMarch, float initialMarchDistance, int numberOfNewtonIterations, bool exitedMetaBall, float stepWidth, inout FragmentValues outValues) +{ + rayMarchWithFragmentValues(rayPosition, rayDirection, stepWidth, initialMarchDistance, numberOfNewtonIterations, isInverseMarch, exitedMetaBall, outValues); + + float illumination = 1.0; + if (outValues.energy > THRESHOLD) { + illumination = calculateIllumination(outValues); + } + + if (isInverseMarch) { + rayDirection = refract(rayDirection, outValues.normal, 1.0 / REFRACTION_INDEX); + } + vec4 texturePositon = rayDirection; + vec4 envMap = texture(u_cubemap, texturePositon.xyz); + + outValues.color = outValues.energy > THRESHOLD ? mix(envMap.xyz, (outValues.color * 2.0) * illumination, METABALL_TRANSPARENCY) : envMap.xyz; + //outValues.color = envMap.xyz; + return illumination; +} + +void prepareMetaballsArray() +{ + if (u_metaballsColorTextureSize >= METABALL_BUFFER_SIZE) + { + discard; + } + for (int metaballIndex = 0; metaballIndex < u_metaballsTextureSize; metaballIndex++) { + // Position and Energy + // We need to double the index since the texture also includes the velocity per metaball => 2 texels per metaball + vec4 metaball = texelFetch(u_metaballsTexture, ivec2(metaballIndex * 2, 0), 0); + metaball.w *= BASE_ENERGY; + metaballArray[metaballIndex] = metaball; + // Color + metaballColorArray[metaballIndex] = texelFetch(u_metaballColorsTexture, ivec2(metaballIndex, 0), 0).xyz; + } +} + +void main(void) +{ + // buffer the metaball texture data in a static array with max size and pass the acutual number of metaballs per uniform. + // parameter optimieren und physics shader rein machen, keine IBL, random metaballs / colors. + prepareMetaballsArray(); + vec3 ray = normalize(v_ray.xyz); + //ray = normalize(ray) * 2.0; + // Compute the color + vec4 rayPosition = v_origin; + float deleteme = 0.0; + //vec4 rayPosition = vec4(-4.0 * ray, 1.0); + FragmentValues fragmentValues; + fullRayMarchWithLightCalculation(rayPosition, vec4(ray, 0.0), false, 2.0, 7, true, 0.05, fragmentValues); + vec3 reflectionDir; + vec3 refractionAndReflectionColor; + FragmentValues refractionValues; + FragmentValues reflectionValues; + float reflectionPercentage; + vec3 refractionDir; + if (fragmentValues.energy > THRESHOLD) { + reflectionDir = reflect(ray, fragmentValues.normal.xyz); + fullRayMarchWithLightCalculation(fragmentValues.rayPosition, vec4(reflectionDir, 0.0), false, 0.05, 7, false, 0.1, reflectionValues); + + refractionDir = refract(ray, fragmentValues.normal.xyz, REFRACTION_INDEX); + if (refractionDir != vec3(0.0)) { + deleteme = fullRayMarchWithLightCalculation(fragmentValues.rayPosition, vec4(refractionDir, 0.0), true, 0.01, 5, true, 0.1, refractionValues); + reflectionPercentage = fresnelReflection(ray, fragmentValues.normal.xyz); + refractionAndReflectionColor = mix(refractionValues.color, reflectionValues.color, reflectionPercentage); + } + else { + refractionAndReflectionColor = reflectionValues.color; + } + } + else { + refractionAndReflectionColor = fragmentValues.color; + } + vec4 finalColor = vec4(mix(fragmentValues.color, refractionAndReflectionColor, REFRACTION_AND_REFLECTION_INTENSITY), 1.0); + fragColor = finalColor; +} + +// NOTE for compilation errors look at the line number and subtract 7 diff --git a/demos/metaballs/metaballs.ts b/demos/metaballs/metaballs.ts new file mode 100644 index 00000000..9e2664ee --- /dev/null +++ b/demos/metaballs/metaballs.ts @@ -0,0 +1,380 @@ +/* spellchecker: disable */ + +import { vec3 } from 'webgl-operate'; + +import { + Camera, + Canvas, + Context, + DefaultFramebuffer, + EventProvider, + Framebuffer, + Invalidate, + Navigation, + NdcFillingTriangle, + Program, + Renderer, + Shader, + TextureCube, + Texture2D, + Wizard, +} from 'webgl-operate'; + +import { Demo } from '../demo'; + +/* spellchecker: enable */ + +// tslint:disable:max-classes-per-file + + +export class MetaballsRenderer extends Renderer { + + protected _uInverseViewProjection: WebGLUniformLocation; + protected _camera: Camera; + protected _navigation: Navigation; + + protected _ndcTriangle: NdcFillingTriangle; + protected _cubeMap: TextureCube; + protected _metaballsTexture: Texture2D; + protected numberOfMetaballs = 7; + protected numberOfMetaballAttributes = 2; // number of vec4's per metaball. + protected _metaballsPhysicsTexture: Texture2D; + protected _metaballColorsTexture: Texture2D; + protected _lightsTexture: Texture2D; + + protected _program: Program; + protected _physicsProgram: Program; + protected _defaultFBO: DefaultFramebuffer; + protected _physicsFBO: Framebuffer; + protected _lastRenderingTime = 0.0; + + protected onUpdate(): boolean { + + // Update camera navigation (process events) + this._navigation.update(); + //return this._altered.any || this._camera.altered; + return true; + } + + protected onDiscarded(): void { + this._altered.alter('canvasSize'); + this._altered.alter('clearColor'); + this._altered.alter('frameSize'); + } + + protected onPrepare(): void { + if (this._altered.canvasSize) { + this._camera.aspect = this._canvasSize[0] / this._canvasSize[1]; + this._camera.viewport = this._canvasSize; + } + + if (this._altered.clearColor) { + this._defaultFBO.clearColor(this._clearColor); + } + + this._altered.reset(); + this._camera.altered = false; + } + + protected renderPhysicsToTexture(): void { + const gl = this._context.gl; + const targetTextureWidth = this.numberOfMetaballs * this.numberOfMetaballAttributes; + const targetTextureHeight = 1; + + this._physicsFBO.bind(); + this._physicsFBO.attachTexture(gl.COLOR_ATTACHMENT0, this._metaballsPhysicsTexture); + gl.viewport(0, 0, targetTextureWidth, targetTextureHeight); + + this._physicsProgram.bind(); + + this._metaballsTexture.bind(gl.TEXTURE0); + + const newTime = new Date().getTime() / 1000; + const deltaTime = newTime - this._lastRenderingTime; + gl.uniform1f(this._physicsProgram.uniform('u_deltaTime'), deltaTime); + + this._lastRenderingTime = newTime; + + + // render geometry + this._ndcTriangle.bind(); + this._ndcTriangle.draw(); + this._ndcTriangle.unbind(); + + this._metaballsTexture.unbind(); + this._physicsProgram.unbind(); + + const data = new Float32Array(targetTextureWidth * targetTextureHeight * 4); + gl.readPixels(0, 0, targetTextureWidth, targetTextureHeight, gl.RGBA, gl.FLOAT, data); + console.log(data); + this._physicsFBO.unbind(); + } + + protected onFrame(): void { + const gl = this._context.gl; + + this.renderPhysicsToTexture(); + this.performTexturePingPong(); + + this._defaultFBO.bind(); + this._defaultFBO.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT, true, false); + + gl.viewport(0, 0, this._frameSize[0], this._frameSize[1]); + + gl.enable(gl.CULL_FACE); + gl.cullFace(gl.BACK); + gl.enable(gl.DEPTH_TEST); + + // Bind Program + this._program.bind(); + + // Bind textures and uniforms + this._metaballsPhysicsTexture.bind(gl.TEXTURE0); + this._metaballColorsTexture.bind(gl.TEXTURE1); + this._lightsTexture.bind(gl.TEXTURE2); + this._cubeMap.bind(gl.TEXTURE3); + + gl.uniformMatrix4fv(this._uInverseViewProjection, gl.GL_FALSE, this._camera.viewProjectionInverse); + + // render geometry + this._ndcTriangle.bind(); + this._ndcTriangle.draw(); + this._ndcTriangle.unbind(); + + this._program.unbind(); + + //const data = new Uint8Array(this._frameSize[0] * this._frameSize[1] * 4); + //gl.readPixels(0, 0, this._frameSize[0], this._frameSize[1], gl.RGBA, gl.UNSIGNED_BYTE, data); + //console.log(data); + + gl.cullFace(gl.BACK); + gl.disable(gl.CULL_FACE); + } + + protected performTexturePingPong(): void { + const tempReference = this._metaballsPhysicsTexture; + this._metaballsPhysicsTexture = this._metaballsTexture; + this._metaballsTexture = tempReference; + } + + protected onSwap(): void { } + + protected onInitialize(context: Context, callback: Invalidate, + eventProvider: EventProvider): boolean { + + this._defaultFBO = new DefaultFramebuffer(this._context, 'DefaultFBO'); + this._defaultFBO.initialize(); + this._defaultFBO.bind(); + + const gl = context.gl; + + this._ndcTriangle = new NdcFillingTriangle(this._context); + this._ndcTriangle.initialize(); + + const vert = new Shader(context, gl.VERTEX_SHADER, 'metaballs.vert'); + vert.initialize(require('./metaballs.vert')); + const frag = new Shader(context, gl.FRAGMENT_SHADER, 'metaballs.frag'); + frag.initialize(require('./metaballs.frag')); + + this._program = new Program(context, 'MetaballsProgram'); + this._program.initialize([vert, frag], false); + this._program.attribute('a_vertex', this._ndcTriangle.vertexLocation); + + this._program.link(); + this._program.bind(); + + this.createMetaballsTexture(); + this.createLightsTexture(); + this.createCubeMapTexture(); + this.setUpPhysicsRendering(); + + this._uInverseViewProjection = this._program.uniform('u_inverseViewProjection'); + this._camera = new Camera(); + this._camera.eye = vec3.fromValues(0.0, 0.5, -4.0); + this._camera.center = vec3.fromValues(0.0, 0.4, 0.0); + this._camera.up = vec3.fromValues(0.0, 1.0, 0.0); + this._camera.near = 0.1; + this._camera.far = 4.0; + + this._navigation = new Navigation(callback, eventProvider); + this._navigation.camera = this._camera; + + this._lastRenderingTime = new Date().getTime() / 1000; + + return true; + } + + protected onUninitialize(): void { + super.uninitialize(); + this._cubeMap.uninitialize(); + this._ndcTriangle.uninitialize(); + + this._program.uninitialize(); + + this._defaultFBO.uninitialize(); + } + + protected setUpPhysicsRendering(): void { + const gl = this._context.gl; + const targetTextureWidth = this.numberOfMetaballs * this.numberOfMetaballAttributes; + const targetTextureHeight = 1; + + this._metaballsPhysicsTexture = new Texture2D(this._context, 'metaballsPhysics'); + this._metaballsPhysicsTexture.initialize(targetTextureWidth, targetTextureHeight, + gl.RGBA32F, gl.RGBA, gl.FLOAT); + this._metaballsPhysicsTexture.filter(gl.NEAREST, gl.NEAREST); + + this._physicsFBO = new Framebuffer(this._context, 'PhysicsFBO'); + this._physicsFBO.initialize([[gl.COLOR_ATTACHMENT0, this._metaballsPhysicsTexture]]); + this._physicsFBO.resize(targetTextureWidth, targetTextureHeight); + + const vert = new Shader(this._context, gl.VERTEX_SHADER, 'physics.vert'); + vert.initialize(require('./physics.vert')); + const frag = new Shader(this._context, gl.FRAGMENT_SHADER, 'physics.frag'); + frag.initialize(require('./physics.frag')); + + this._physicsProgram = new Program(this._context, 'MetaballsPhysicsProgram'); + this._physicsProgram.initialize([vert, frag], false); + this._physicsProgram.attribute('a_vertex', this._ndcTriangle.vertexLocation); + + this._physicsFBO.bind(); + gl.viewport(0, 0, targetTextureWidth, targetTextureHeight); + + this._physicsProgram.link(); + this._physicsProgram.bind(); + + gl.uniform1i(this._physicsProgram.uniform('u_metaballsTexture'), 0); + gl.uniform1i(this._physicsProgram.uniform('u_metaballsTextureSize'), + this.numberOfMetaballs * this.numberOfMetaballAttributes); + } + + protected createMetaballsTexture(): void { + // const metaballs = new Float32Array(numberOfMetaballs * 4); + // metaballs.forEach((val, i, array) => array[i] = Math.random() + 0.5); + const metaballs = new Float32Array(this.numberOfMetaballs * this.numberOfMetaballAttributes * 4); + for (let i = 0; i < this.numberOfMetaballs * 4 * this.numberOfMetaballAttributes; i += 8) { + metaballs[i + 0] = Math.random() * 2.0 - 1.0; + metaballs[i + 1] = Math.random() * 2.0 - 1.0; + metaballs[i + 2] = Math.random() * 2.0 - 1.0; + metaballs[i + 3] = 0.7; + } + /*const metaballs = new Float32Array([ + // x, y, z, metaball-energy, velocity x, y, z, placeholder + -0.3, -0.3, 0.9, 0.7, 0.0, 0.0, 0.0, 0.0, + -0.8, 0.1, 0.4, 0.7, 0.0, 0.0, 0.0, 0.0, + 0.4, -0.4, 0.6, 0.7, 0.0, 0.0, 0.0, 0.0, + 0.5, 0.7, 0.2, 0.7, 0.0, 0.0, 0.0, 0.0, + -0.5, 0.5, 0.2, 0.7, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.5, 0.2, 0.7, 0.0, 0.0, 0.0, 0.0, + 0.2, -0.1, -0.5, 0.5, 0.0, 0.0, 0.0, 0.0, + ]);*/ + + const metaballColors = new Float32Array(this.numberOfMetaballs * 4); + for (let i = 0; i < this.numberOfMetaballs * 4; i += 4) { + metaballColors[i + 0] = (Math.random() / 2.0) + 0.5; //r + metaballColors[i + 1] = (Math.random() / 2.0) + 0.5; //g + metaballColors[i + 2] = (Math.random() / 2.0) + 0.5; //b + metaballColors[i + 3] = 1.0; //a + } + //console.log(metaballColors); + //metaballColors.forEach((val, i, array) => array[i] = Math.random()); + /*const metaballColors = new Float32Array([ + // r, g, b, a + 0.105, 0.768, 0.011, 1.0, + 0.968, 0.411, 0.737, 1.0, + 0.325, 0.454, 0.992, 1.0, + 0.986, 0.274, 0.290, 1.0, + 0.986, 0.274, 0.290, 1.0, + 0.968, 0.411, 0.737, 1.0, + 0.105, 0.768, 0.011, 1.0, + ]);*/ + const gl = this._context.gl; + + this._metaballsTexture = new Texture2D(this._context, 'metaballsTexture'); + this._metaballsTexture.initialize(metaballs.length / 4, 1, gl.RGBA32F, gl.RGBA, gl.FLOAT); + this._metaballsTexture.filter(gl.NEAREST, gl.NEAREST); + this._metaballsTexture.data(metaballs); + gl.uniform1i(this._program.uniform('u_metaballsTexture'), 0); + gl.uniform1i(this._program.uniform('u_metaballsTextureSize'), metaballs.length / 4); + + this._metaballColorsTexture = new Texture2D(this._context, 'metaballColorsTexture'); + this._metaballColorsTexture.initialize(metaballColors.length / 4, 1, gl.RGBA32F, gl.RGBA, gl.FLOAT); + this._metaballColorsTexture.filter(gl.NEAREST, gl.NEAREST); + this._metaballColorsTexture.data(metaballColors); + gl.uniform1i(this._program.uniform('u_metaballColorsTexture'), 1); + gl.uniform1i(this._program.uniform('u_metaballsColorTextureSize'), metaballColors.length / 4); + } + + protected createLightsTexture(): void { + const lights = new Float32Array([ + // x, y, z, shininess-factor + -2.5, -2.5, 0.5, 100.0, + 0.0, -2.5, -0.5, 100.0, + ]); + const numberOfLights = lights.length / 4; + const gl = this._context.gl; + + this._lightsTexture = new Texture2D(this._context, 'lightsTexture'); + this._lightsTexture.initialize(numberOfLights, 1, gl.RGBA32F, gl.RGBA, gl.FLOAT); + this._lightsTexture.filter(gl.NEAREST, gl.NEAREST); + this._lightsTexture.data(lights); + gl.uniform1i(this._program.uniform('u_lightsTexture'), 2); + gl.uniform1i(this._program.uniform('u_lightsTextureSize'), numberOfLights); + } + + protected createCubeMapTexture(): void { + const gl = this._context.gl; + + gl.uniform1i(this._program.uniform('u_cubemap'), 3); + const internalFormatAndType = Wizard.queryInternalTextureFormat( + this._context, gl.RGB, Wizard.Precision.byte); + this._cubeMap = new TextureCube(this._context); + this._cubeMap.initialize(592, internalFormatAndType[0], gl.RGB, internalFormatAndType[1]); + this._cubeMap.fetch({ + positiveX: '/demos/data/cube-map-px.jpg', negativeX: '/demos/data/cube-map-nx.jpg', + positiveY: '/demos/data/cube-map-py.jpg', negativeY: '/demos/data/cube-map-ny.jpg', + positiveZ: '/demos/data/cube-map-pz.jpg', negativeZ: '/demos/data/cube-map-nz.jpg', + }).then(() => { + const gl = this._context.gl; + this._cubeMap.filter(gl.LINEAR, gl.LINEAR, true, true); + + this.invalidate(true); + }); + } +} + + +export class MetaballsDemo extends Demo { + + private _canvas: Canvas; + private _renderer: MetaballsRenderer; + + onInitialize(element: HTMLCanvasElement | string): boolean { + + this._canvas = new Canvas(element); + this._canvas.controller.multiFrameNumber = 1; + this._canvas.framePrecision = Wizard.Precision.float; + this._canvas.clearColor.fromHex('d6d8db'); + this._canvas.frameScale = [1.0, 1.0]; + + this._canvas.element.addEventListener('click', () => { this._canvas.controller.update(); }); + + this._renderer = new MetaballsRenderer(); + this._canvas.renderer = this._renderer; + + return true; + } + + onUninitialize(): void { + this._canvas.dispose(); + (this._renderer as Renderer).uninitialize(); + } + + get canvas(): Canvas { + return this._canvas; + } + + get renderer(): MetaballsRenderer { + return this._renderer; + } +} diff --git a/demos/metaballs/metaballs.vert b/demos/metaballs/metaballs.vert new file mode 100644 index 00000000..c3d8bf6f --- /dev/null +++ b/demos/metaballs/metaballs.vert @@ -0,0 +1,29 @@ + +precision lowp float; + +@import ../../source/shaders/facade.vert; + + +#if __VERSION__ == 100 + attribute vec2 a_vertex; +#else + layout(location = 0) in vec2 a_vertex; +#endif + +uniform mat4 u_inverseViewProjection; + +varying vec4 v_ray; +varying vec4 v_origin; + +void main() +{ + //v_ray = u_inverseViewProjection * vec4(a_vertex.x, 0.0, a_vertex.y, 0.0); + v_ray = u_inverseViewProjection * vec4(a_vertex.xy, 1.0, 1.0); + v_ray /= v_ray.w; + v_origin = u_inverseViewProjection * vec4(a_vertex.xy, -1.0, 1.0); + v_origin /= v_origin.w; + + v_ray -= v_origin; + + gl_Position = vec4(a_vertex, 0.0, 1.0); +} diff --git a/demos/metaballs/physics.frag b/demos/metaballs/physics.frag new file mode 100644 index 00000000..36f35d27 --- /dev/null +++ b/demos/metaballs/physics.frag @@ -0,0 +1,137 @@ +precision lowp float; + +@import ../../source/shaders/facade.frag; + + +#if __VERSION__ == 100 + #define fragColor gl_FragColor +#else + layout(location = 0) out vec4 fragColor; +#endif + +# define TIME_SCALE_FACTOR 0.1 + +uniform sampler2D u_metaballsTexture; +uniform int u_metaballsTextureSize; +uniform float u_deltaTime; + + +// wie viele voxel pro dimension +# define FORCE_VECTOR_FIELD_DIMENSION_SIZE 3 +// breite une hoehe des cubes in Einheit +# define FORCE_VECTOR_FIELD_EXTENSION 2.0 + +vec3 forceVectorField[FORCE_VECTOR_FIELD_DIMENSION_SIZE * FORCE_VECTOR_FIELD_DIMENSION_SIZE * FORCE_VECTOR_FIELD_DIMENSION_SIZE] = vec3[FORCE_VECTOR_FIELD_DIMENSION_SIZE * FORCE_VECTOR_FIELD_DIMENSION_SIZE * FORCE_VECTOR_FIELD_DIMENSION_SIZE]( + // negative plane: y = -1 + // x = variable, y = -1, z = -1 + vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), + // x = variable, y = -1, z = 0 + vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, -1.0), vec3(0.0, 0.0, 1.0), + // x = variable, y = -1, z = 1 + vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), + + // neutral plane: y = 0 + // x = variable, y = 0, z = -1 + vec3(0.0, -1.0, 0.0), vec3(-1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), + // x = variable, y = 0, z = 0 + vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0), vec3(-1.0, 0.0, 0.0), + // x = variable, y = 0, z = 1 + vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(-1.0, 0.0, 0.0), + + // positive plane: y = 1 + // x = variable, y = 1, z = -1 + vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0), + // x = variable, y = 1, z = 0 + vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0 ,1.0), + // x = variable, y = 1, z = -1 + vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0)); + +const vec3 forceVectorFieldOffset = vec3(FORCE_VECTOR_FIELD_EXTENSION, FORCE_VECTOR_FIELD_EXTENSION, FORCE_VECTOR_FIELD_EXTENSION) / 2.0; +const float forceVectorVoxelSize = FORCE_VECTOR_FIELD_EXTENSION / float(FORCE_VECTOR_FIELD_DIMENSION_SIZE); + +vec3 fetchForceVector(ivec3 voxel) { + // if the voxel is not inside the force field return 0 as force vector. + if (any(lessThan(voxel, ivec3(0))) || any(greaterThanEqual(voxel, ivec3(FORCE_VECTOR_FIELD_DIMENSION_SIZE)))) { + return vec3(0.0); + } + int index = voxel.x + voxel.z * FORCE_VECTOR_FIELD_DIMENSION_SIZE + voxel.y * FORCE_VECTOR_FIELD_DIMENSION_SIZE * FORCE_VECTOR_FIELD_DIMENSION_SIZE; + return forceVectorField[index]; +} + +vec3 getForceVector(vec3 metaballPosition) { + + vec3 positionInForceField = (metaballPosition + forceVectorFieldOffset) * forceVectorVoxelSize; + ivec3 positionInForceFieldFloor = clamp(ivec3(floor(positionInForceField)), 0, FORCE_VECTOR_FIELD_DIMENSION_SIZE - 1); + ivec3 positionInForceFieldCeil = clamp(ivec3(ceil(positionInForceField)), 0, FORCE_VECTOR_FIELD_DIMENSION_SIZE -1); + + ivec3 conceringVoxels[8] = ivec3[8]( + ivec3(positionInForceFieldFloor.x, positionInForceFieldFloor.y, positionInForceFieldFloor.z), + ivec3(positionInForceFieldFloor.x, positionInForceFieldFloor.y, positionInForceFieldCeil.z), + ivec3(positionInForceFieldFloor.x, positionInForceFieldCeil.y, positionInForceFieldFloor.z), + ivec3(positionInForceFieldFloor.x, positionInForceFieldCeil.y, positionInForceFieldCeil.z), + ivec3(positionInForceFieldCeil.x, positionInForceFieldFloor.y, positionInForceFieldFloor.z), + ivec3(positionInForceFieldCeil.x, positionInForceFieldFloor.y, positionInForceFieldCeil.z), + ivec3(positionInForceFieldCeil.x, positionInForceFieldCeil.y, positionInForceFieldFloor.z), + ivec3(positionInForceFieldCeil.x, positionInForceFieldCeil.y, positionInForceFieldCeil.z) + ); + + vec3 conceringForceVectors[8] = vec3[8]( + fetchForceVector(conceringVoxels[0]), + fetchForceVector(conceringVoxels[1]), + fetchForceVector(conceringVoxels[2]), + fetchForceVector(conceringVoxels[3]), + fetchForceVector(conceringVoxels[4]), + fetchForceVector(conceringVoxels[5]), + fetchForceVector(conceringVoxels[6]), + fetchForceVector(conceringVoxels[7]) + ); + + vec3 forceVector = vec3(0.0); + for (int i = 0; i < 8; i++) + { + vec3 currentVector = conceringForceVectors[i]; + if (currentVector != vec3(0.0)) + { + forceVector += currentVector * distance(positionInForceField, vec3(conceringVoxels[i])); + } + } + + // individual force vector + int individualId = int(gl_FragCoord.x); + float x = float(individualId % 3 - 1) * 2.0; + float y = float(individualId % 5 - 2); + float z = float(individualId % 3 - 1) * 2.0; + vec3 individualVector = normalize(vec3(x, y, z)); + return normalize(mix(forceVector, individualVector, 0.5)); +} + +vec3 calculateNewPositionAndVelocity(inout vec4 positionAndMass, inout vec3 velocity) { + vec3 acceleration = getForceVector(positionAndMass.xyz) / positionAndMass.w; + velocity = velocity + acceleration * u_deltaTime * TIME_SCALE_FACTOR; + positionAndMass.xyz = positionAndMass.xyz + velocity * u_deltaTime * TIME_SCALE_FACTOR; + positionAndMass.xyz = clamp(positionAndMass.xyz, vec3(-FORCE_VECTOR_FIELD_EXTENSION / 2.0), vec3(FORCE_VECTOR_FIELD_EXTENSION / 2.0)); + return velocity; +} + +vec4 getCurrentVelocity(vec2 texCoords) { + texCoords += vec2(1.0 / float(u_metaballsTextureSize), 0.0); + return texture(u_metaballsTexture, texCoords); +} + +vec4 getCurrentPosition(vec2 texCoords) { + return texture(u_metaballsTexture, texCoords); +} + +void main(void) +{ + ivec2 texCoords = ivec2(gl_FragCoord.xy); + bool isVelocityFragment = bool(mod(gl_FragCoord.x - 0.5, 2.0)); + texCoords = isVelocityFragment ? texCoords - ivec2(1,0) : texCoords; + + vec3 velocity = texelFetch(u_metaballsTexture, texCoords + ivec2(1, 0), 0).xyz; + vec4 positionAndMass = texelFetch(u_metaballsTexture, texCoords, 0); + vec3 acc = calculateNewPositionAndVelocity(positionAndMass, velocity); + fragColor = isVelocityFragment ? vec4(velocity, 1.0) : positionAndMass; +} + +// NOTE for compilation errors look at the line number and subtract 7 diff --git a/demos/metaballs/physics.vert b/demos/metaballs/physics.vert new file mode 100644 index 00000000..bada432b --- /dev/null +++ b/demos/metaballs/physics.vert @@ -0,0 +1,16 @@ + +precision lowp float; + +@import ../../source/shaders/facade.vert; + + +#if __VERSION__ == 100 + attribute vec2 a_vertex; +#else + layout(location = 0) in vec2 a_vertex; +#endif + +void main() +{ + gl_Position = vec4(a_vertex, 0.0, 1.0); +} diff --git a/source/framebuffer.ts b/source/framebuffer.ts index f28fb26d..fb68f3cb 100644 --- a/source/framebuffer.ts +++ b/source/framebuffer.ts @@ -404,6 +404,21 @@ export class Framebuffer extends AbstractObject implements Bin return this._texturesByAttachment.get(attachment); } + /** + * Attach a texture after the object is already initialized. + * @param attachment - The attachment where the texture object should be bind to. + * @param attachment - The attachment to request the texture object of. + * @returns - A texture object if one exists for the given attachment, otherwise undefined. + */ + @Initializable.assert_initialized() + public attachTexture(attachment: GLenum, texture: Texture2D): void { + + const gl = this.context.gl; + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, texture.object, 0); + this._texturesByAttachment.set(attachment, texture); + } + + /** * Forwards a resize to all attachments, renderbuffers and textures. * @param width - Targeted/new width for all attachments in px. diff --git a/source/shaders/env-projections.frag b/source/shaders/env-projections.frag index 115c9f53..7ada10a3 100644 --- a/source/shaders/env-projections.frag +++ b/source/shaders/env-projections.frag @@ -58,7 +58,6 @@ void main(void) fragColor = texture(u_equirectmap, uv); #endif - #if defined(SPHERE_MAP) ray = -ray.xzy; ray.y *= -1.0; diff --git a/webpack.config.demos.js b/webpack.config.demos.js index c720b8f6..1000cd3f 100644 --- a/webpack.config.demos.js +++ b/webpack.config.demos.js @@ -9,6 +9,7 @@ module.exports = { 'gltf-renderer': ['gltf-renderer/gltfrenderer.ts'], 'progressive-lighting': ['progressive-lighting/progressive-lighting.ts'], 'eye-tracking': ['eye-tracking/eyetracking.ts'], + 'metaballs': ['metaballs/metaballs.ts'], 'point-cloud': ['point-cloud/point-cloud.ts'] }, devtool: 'source-map', diff --git a/website/demos/metaballs.pug b/website/demos/metaballs.pug new file mode 100644 index 00000000..6e6350ab --- /dev/null +++ b/website/demos/metaballs.pug @@ -0,0 +1,9 @@ +extends demo.pug + + +block canvas + + canvas(id = 'demo-canvas', data-backend = 'auto').img-fluid.w-100 + +block onload + script window.onload = function () { clipboard(); demo(new MetaballsDemo(), 'demo-canvas'); }