Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions python/MeshDemo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,91 @@
"mrender.image().resize((width//2, height//2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Screen-Space Ambient Occlusion (SSAO) Demo"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Enable SSAO to add realistic ambient occlusion shadows"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Create a new renderer with SSAO enabled\n",
"mrender_ssao = ogl.MeshRenderer(width, height)\n",
"mrender_ssao.setMesh(P, None, N, C)\n",
"\n",
"mrender_ssao.lookAt(*camParams)\n",
"mrender_ssao.modelMatrix(*modParams)\n",
"mrender_ssao.perspective(50, width / height, 0.1, 2000)\n",
"\n",
"mrender_ssao.meshes[0].alpha = 0.5\n",
"mrender_ssao.meshes[0].lineWidth = 1.5\n",
"mrender_ssao.meshes[0].shininess = 100.0\n",
"mrender_ssao.specularIntensity[:] = 2.0\n",
"\n",
"# Enable SSAO with custom parameters\n",
"mrender_ssao.enableSSAO(enabled=True, radius=0.5, bias=0.025, samples=16)\n",
"\n",
"# Render and display\n",
"mrender_ssao.render()\n",
"mrender_ssao.image().resize((width//2, height//2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Infinite Ground Plane Demo"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Add an infinite ground plane at z=0 that only displays ambient occlusion"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Create a new renderer with ground plane\n",
"mrender_ground = ogl.MeshRenderer(width, height)\n",
"mrender_ground.setMesh(P, None, N, C)\n",
"\n",
"mrender_ground.lookAt(*camParams)\n",
"mrender_ground.modelMatrix(*modParams)\n",
"mrender_ground.perspective(50, width / height, 0.1, 2000)\n",
"\n",
"mrender_ground.meshes[0].alpha = 0.5\n",
"mrender_ground.meshes[0].lineWidth = 1.5\n",
"mrender_ground.meshes[0].shininess = 100.0\n",
"mrender_ground.specularIntensity[:] = 2.0\n",
"\n",
"# Enable SSAO\n",
"mrender_ground.enableSSAO(enabled=True, radius=0.5, bias=0.025, samples=16)\n",
"\n",
"# Add an infinite ground plane at z=0 that only shows AO\n",
"mrender_ground.addInfiniteGroundPlane(z=0.0, size=1000.0, only_show_ao=True)\n",
"\n",
"# Render and display\n",
"mrender_ground.render()\n",
"mrender_ground.image().resize((width//2, height//2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
126 changes: 119 additions & 7 deletions python/OffscreenRenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,16 @@ def lookAtMatrix(position, target, up):
return matView

class Mesh:
def __init__(self, ctx, V, F, N, color):
def __init__(self, ctx, V, F, N, color, use_ssao=False):
self.ctx = ctx
self.shader = ctx.shaderLibrary().load(SHADER_DIR + '/phong_with_wireframe.vert',
SHADER_DIR + '/phong_with_wireframe.frag')
self.use_ssao = use_ssao
# Load SSAO shader if requested, otherwise use regular shader
if use_ssao:
self.shader = ctx.shaderLibrary().load(SHADER_DIR + '/phong_with_wireframe.vert',
SHADER_DIR + '/phong_with_wireframe_ssao.frag')
else:
self.shader = ctx.shaderLibrary().load(SHADER_DIR + '/phong_with_wireframe.vert',
SHADER_DIR + '/phong_with_wireframe.frag')

# The triangle index array in active use to replicate vertex data to
# per-corner data.
Expand All @@ -105,6 +111,12 @@ def __init__(self, ctx, V, F, N, color):
self.setWireframe(0.0)
self.matModel = np.identity(4)
self.shininess = 20.0

# SSAO parameters (use floats for shader compatibility)
self.ssaoEnabled = 1.0 if use_ssao else 0.0
self.ssaoRadius = 0.5
self.ssaoBias = 0.025
self.ssaoSamples = 16.0

self.vao = None

Expand Down Expand Up @@ -283,6 +295,13 @@ def render(self, matView):
self.shader.setUniform('alpha', self.alpha)

self.shader.setUniform('lineWidth', self.lineWidth)

# Set SSAO uniforms only if using SSAO shader
if self.use_ssao:
self.shader.setUniform('ssaoEnabled', self.ssaoEnabled)
self.shader.setUniform('ssaoRadius', self.ssaoRadius)
self.shader.setUniform('ssaoBias', self.ssaoBias)
self.shader.setUniform('ssaoSamples', self.ssaoSamples)

# Any constant color configured is not part of the VAO state and must be set again to ensure it hasn't been overwritten
if self.constColor: self.vao.setConstantAttribute(2, self.color)
Expand Down Expand Up @@ -332,6 +351,15 @@ def __init__(self, width, height):
self.specularIntensity = 1.0 * white

self.transparentBackground = True

# SSAO settings (use floats for shader compatibility)
self.ssaoEnabled = 0.0 # Default: disabled
self.ssaoRadius = 0.5
self.ssaoBias = 0.025
self.ssaoSamples = 16.0

# Ground plane settings
self.infiniteGroundPlane = None

def resize(self, width, height):
self.ctx.resize(width, height)
Expand All @@ -342,15 +370,20 @@ def setMesh(self, V, F, N, color, which = 0):
`F` can be `None` to disable indexed face set representation
(i.e., to use glDrawArrays instead of glDrawElements)
"""
if len(self.meshes) == 0: self.meshes = [Mesh(self.ctx, V, F, N, color)]
else: self.meshes[which].setMesh(V, F, N, color)
if len(self.meshes) == 0:
self.meshes = [Mesh(self.ctx, V, F, N, color, use_ssao=(self.ssaoEnabled > 0.5))]
else:
self.meshes[which].setMesh(V, F, N, color)

def addMesh(self, V, F, N, color, makeDefault = True):
def addMesh(self, V, F, N, color, makeDefault = True, use_ssao=None):
"""
Add a mesh to the scene. Arguments are the same as `setMesh`.
By default, the new mesh becomes the active default one (index 0).
If use_ssao is None, uses the renderer's ssaoEnabled setting.
"""
self.meshes.insert(0 if makeDefault else len(self.meshes), Mesh(self.ctx, V, F, N, color))
if use_ssao is None:
use_ssao = (self.ssaoEnabled > 0.5)
self.meshes.insert(0 if makeDefault else len(self.meshes), Mesh(self.ctx, V, F, N, color, use_ssao=use_ssao))

def addVectorFieldMesh(self, V, F, N, arrowPos, arrowVec, arrowColor,
arrowRelativeScreenSize, arrowAlignment, targetDepth):
Expand Down Expand Up @@ -433,6 +466,13 @@ def render(self, clear=True, clearColor = None):
self.ctx.blendFunc(GLenum.GL_SRC_ALPHA, GLenum.GL_ONE_MINUS_SRC_ALPHA,
GLenum.GL_ONE, GLenum.GL_ONE_MINUS_SRC_ALPHA)

# Sync SSAO settings to all meshes that support it
for mesh in self.meshes:
if hasattr(mesh, 'ssaoEnabled'):
mesh.ssaoEnabled = self.ssaoEnabled
mesh.ssaoRadius = self.ssaoRadius
mesh.ssaoBias = self.ssaoBias
mesh.ssaoSamples = self.ssaoSamples

# Render the opaque meshes first
# This will result in a perfectly rendered scene with N opaque objects and 1 transparent object.
Expand Down Expand Up @@ -460,6 +500,78 @@ def scaledImage(self, scaleFactor):
img = self.image()
return img.resize((int(img.width * scaleFactor),
int(img.height * scaleFactor)))

def enableSSAO(self, enabled=True, radius=0.5, bias=0.025, samples=16):
"""
Enable or disable Screen-Space Ambient Occlusion (SSAO).

Parameters:
- enabled: Whether to enable SSAO
- radius: The radius of the SSAO sampling sphere
- bias: Bias to prevent self-shadowing artifacts
- samples: Number of samples to take (limited to 16 in shader)
"""
self.ssaoEnabled = 1.0 if enabled else 0.0
self.ssaoRadius = float(radius)
self.ssaoBias = float(bias)
self.ssaoSamples = float(min(samples, 16))

# Update SSAO settings for existing meshes
for mesh in self.meshes:
if hasattr(mesh, 'ssaoEnabled'):
mesh.ssaoEnabled = 1.0 if enabled else 0.0
mesh.ssaoRadius = float(radius)
mesh.ssaoBias = float(bias)
mesh.ssaoSamples = float(min(samples, 16))

def addInfiniteGroundPlane(self, z=0.0, size=1000.0, color=[0.8, 0.8, 0.8], only_show_ao=False):
"""
Add an infinite ground plane at the specified z-coordinate.
The plane only displays ambient occlusion (no direct lighting).

Parameters:
- z: Z-coordinate of the ground plane
- size: Size of the ground plane (should be large enough to appear infinite)
- color: Color of the ground plane
- only_show_ao: If True, the plane is invisible except for AO shadows
"""
# Create a large quad at z=0
half_size = size / 2
V = np.array([
[-half_size, -half_size, z],
[ half_size, -half_size, z],
[ half_size, half_size, z],
[-half_size, half_size, z]
])

F = np.array([
[0, 1, 2],
[0, 2, 3]
], dtype=np.uint32)

# Normal pointing up (positive z)
N = np.array([
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
[0, 0, 1]
])

# If only_show_ao is True, make the plane transparent/invisible in color
if only_show_ao:
plane_color = [0, 0, 0, 0] # Fully transparent
else:
plane_color = color if len(color) == 4 else list(color) + [1.0]

# Add the ground plane mesh with SSAO enabled
self.addMesh(V, F, N, plane_color, makeDefault=False, use_ssao=True)
self.infiniteGroundPlane = self.meshes[-1]

# Enable SSAO for the ground plane
self.infiniteGroundPlane.ssaoEnabled = 1.0 # Always enable for ground plane
self.infiniteGroundPlane.ssaoRadius = self.ssaoRadius
self.infiniteGroundPlane.ssaoBias = self.ssaoBias
self.infiniteGroundPlane.ssaoSamples = self.ssaoSamples

def renderAnimation(self, outPath, nframes, frameCallback, display=False, *videoWriterArgs, **videoWriterKWargs):
"""
Expand Down
100 changes: 100 additions & 0 deletions shaders/phong_with_wireframe_ssao.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Phong shader with wireframe and SSAO support
// Based on phong_with_wireframe.frag with added SSAO
#version 140

// Light and material parameters
uniform vec3 lightEyePos;
uniform vec3 diffuseIntensity;
uniform vec3 ambientIntensity;
uniform vec3 specularIntensity;
uniform float shininess;
uniform float alpha; // Transparency

uniform float lineWidth;

// SSAO parameters
uniform float ssaoEnabled; // Use float instead of bool for compatibility (0.0 = off, 1.0 = on)
uniform float ssaoRadius;
uniform float ssaoBias;
uniform float ssaoSamples;

// Fragment shader inputs
in vec3 v2f_eyePos;
in vec3 v2f_eyeNormal;
in vec4 v2f_color; // Used for ambient, specular, and diffuse reflection constants
in vec4 v2f_wireframe_color;

// For drawing wireframe
noperspective in vec3 v2f_barycentric; // Barycentric coordinate functions.

// Fragment shader output (pixel color)
out vec4 result;

// Simple SSAO approximation using screen-space derivatives
float computeSSAO() {
if (ssaoEnabled < 0.5) return 1.0;

// Use depth-based occlusion approximation
// This is a simplified SSAO that works without additional render passes
vec3 ddxPos = dFdx(v2f_eyePos);
vec3 ddyPos = dFdy(v2f_eyePos);

// Simple ambient occlusion based on local geometry curvature
float occlusion = 0.0;
int samples = int(min(ssaoSamples, 16.0));

const float TWO_PI = 6.283185307;

for (int i = 0; i < 16; i++) {
if (i >= samples) break;

float angle = float(i) * (TWO_PI / 16.0);
float cosA = cos(angle);
float sinA = sin(angle);
vec2 offset = vec2(cosA, sinA) * ssaoRadius;

vec3 samplePos = v2f_eyePos + ddxPos * offset.x + ddyPos * offset.y;
float sampleDepth = length(samplePos);
float currentDepth = length(v2f_eyePos);

// Simple depth comparison
if (sampleDepth < currentDepth - ssaoBias) {
occlusion += 1.0;
}
}

return 1.0 - (occlusion / float(samples));
}

void main() {
vec3 L = normalize(lightEyePos - v2f_eyePos);
vec3 V = normalize(-v2f_eyePos);
// Use unit normal oriented to always point towards the camera
vec3 N = sign(dot(v2f_eyeNormal, V)) * normalize(v2f_eyeNormal);
vec3 R = reflect(-L, N);
float d = max(dot(L, N), 0.0);

// Compute SSAO factor
float aoFactor = computeSSAO();

// Apply SSAO to ambient lighting
vec3 ambientWithAO = ambientIntensity * aoFactor;

vec4 color = v2f_color * vec4(ambientWithAO + d * diffuseIntensity, alpha);
if (d != 0.0) color.rgb += pow(max(dot(R, V), 0.0), shininess) * specularIntensity; // Use white specular highlights regardless of material's color

// Draw a black wireframe if requested
if (lineWidth > 0.0) {
// Clamp now so that the wireframe antialiasing doesn't get blown out.
color = clamp(color, 0.0, 1.0);

// Trick for implementing "Single-pass Wireframe Rendering" method without a geometry shader:
// infer height above each triangle edge using (norms of) barycentrentric coordinate gradients.
vec3 h = v2f_barycentric / sqrt(pow(dFdx(v2f_barycentric), vec3(2.0)) + pow(dFdy(v2f_barycentric), vec3(2.0)));
float dist = min(min(h.x, h.y), h.z) - 0.5 * lineWidth; // Distance to line border (at lineWidth/2 above h=0)
color = mix(color, v2f_wireframe_color,
exp(-pow(max(dist + 0.9124443057840285280, 0.0), 4))); // dist + log(2)^(1/4) centers transition from 1 to 0 around the edge.
}

result = color;
}
2 changes: 1 addition & 1 deletion src/python_bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ if (NOT TARGET pybind11::module)
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11.git
GIT_TAG v2.6.1
GIT_TAG v2.11.1
)
FetchContent_MakeAvailable(pybind11)
# add_subdirectory(${pybind11_SOURCE_DIR} pybind11_bin)
Expand Down