Skip to content

3D sphere example #1660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 26, 2023
Merged
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
177 changes: 177 additions & 0 deletions arcade/examples/gl/3d_sphere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
A 3D sphere example.

We're showing how to use the geometry.sphere() function to create a sphere
and how different context flags affects the rendering of a 3d object.

If Python and Arcade are installed, this example can be run from the command line with:
python -m arcade.examples.gl.3d_sphere
"""
import arcade
from arcade.math import clamp
from arcade.gl import geometry
from pyglet.math import Mat4
from pyglet.graphics import Batch


class Sphere3D(arcade.Window):

def __init__(self, width, height, title):
super().__init__(width, height, title, resizable=True)
self.sphere = geometry.sphere(1.0, 32, 32, uvs=False)
# Simple color lighting program
self.program = self.ctx.program(
vertex_shader="""
#version 330

uniform WindowBlock {
mat4 projection;
mat4 view;
} window;

in vec3 in_position;
in vec3 in_normal;

out vec3 normal;
out vec3 pos;

void main() {
vec4 p = window.view * vec4(in_position, 1.0);
gl_Position = window.projection * p;
mat3 m_normal = transpose(inverse(mat3(window.view)));
normal = m_normal * in_normal;
pos = p.xyz;
}
""",
fragment_shader="""
#version 330

out vec4 fragColor;

in vec3 normal;
in vec3 pos;

void main()
{
float l = dot(normalize(-pos), normalize(normal));
// Draw front and back face differently
if (l < 0.0) {
l = abs(l);
fragColor = vec4(0.75, 0.0, 0.0, 1.0) * (0.25 + abs(l) * 0.75);
} else {
fragColor = vec4(1.0) * (0.25 + abs(l) * 0.75);
}
}
""",
)
self.on_resize(*self.get_size())

self.text_batch = Batch()
self.text_cull = arcade.Text(
"F2: Toggle cull face (true)",
start_x=10, start_y=10, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)
self.text_depth = arcade.Text(
"F1: Toggle depth test (True)",
start_x=10, start_y=30, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)
self.text_wireframe = arcade.Text(
"SPACE: Toggle wireframe (False)",
start_x=10, start_y=50, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)
self.text_fs = arcade.Text(
"F: Toggle fullscreen (False)",
start_x=10, start_y=70, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)
self.text_vert_count = arcade.Text(
"Use mouse wheel to add/remove vertices",
start_x=10, start_y=90, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)
self.text_rotate = arcade.Text(
"Drag mouse to rotate object",
start_x=10, start_y=110, font_size=15, color=arcade.color.WHITE,
batch=self.text_batch
)

self.set_vsync(True)

self.rot_x = 0
self.rot_y = 0
self.wireframe = True
self.vert_count = 0.5
self.drag = False
self.time = 0
self.flags = set([self.ctx.DEPTH_TEST])

def on_draw(self):
self.clear()
self.ctx.enable_only(*self.flags)
self.ctx.wireframe = self.wireframe

# Position and rotate the sphere
translate = Mat4.from_translation((0, 0, -2.5))
rx = Mat4.from_rotation(self.time + self.rot_x, (0, 1, 0))
ry = Mat4.from_rotation(self.time + self.rot_y, (1, 0, 0))
# Set matrices and draw
self.view = translate @ rx @ ry
self.projection = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=60)
self.sphere.render(self.program, vertices=int(self.sphere.num_vertices * self.vert_count))

# Switch to 2D mode when drawing text
self.projection = Mat4.orthogonal_projection(0, self.width, 0, self.height, -1, 1)
self.ctx.disable(self.ctx.DEPTH_TEST, self.ctx.CULL_FACE)
self.ctx.wireframe = False
self.view = Mat4()

with self.ctx.enabled_only():
self.text_batch.draw()

def on_update(self, dt):
if not self.drag:
self.time += dt / 2

def on_key_press(self, key, modifiers):
if key == arcade.key.ESCAPE:
self.close()
elif key == arcade.key.F:
self.set_fullscreen(not self.fullscreen)
elif key == arcade.key.SPACE:
self.wireframe = not self.wireframe
self.set_vsync(True)
elif key == arcade.key.F1:
if self.ctx.DEPTH_TEST in self.flags:
self.flags.remove(self.ctx.DEPTH_TEST)
else:
self.flags.add(self.ctx.DEPTH_TEST)
elif key == arcade.key.F2:
if self.ctx.CULL_FACE in self.flags:
self.flags.remove(self.ctx.CULL_FACE)
else:
self.flags.add(self.ctx.CULL_FACE)

self.text_wireframe.text = f"SPACE: Toggle wireframe ({self.ctx.wireframe})"
self.text_fs.text = f"F: Toggle fullscreen ({self.fullscreen})"
self.text_depth.text = f"F1: Toggle depth test ({self.ctx.DEPTH_TEST in self.flags})"
self.text_cull.text = f"F2: Toggle cull face ({self.ctx.CULL_FACE in self.flags})"

def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
self.drag = True
self.rot_x += dx / 100
self.rot_y -= dy / 100

def on_mouse_release(self, x, y, button, modifiers):
self.drag = False

def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int):
self.vert_count = clamp(self.vert_count + scroll_y / 500, 0.0, 1.0)


if __name__ == "__main__":
window = Sphere3D(1280, 720, "3D Cube")
window.set_vsync(True)
arcade.run()
77 changes: 77 additions & 0 deletions arcade/gl/geometry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
A module providing commonly used geometry
"""
import math
from array import array
from typing import Tuple

Expand Down Expand Up @@ -206,3 +207,79 @@ def cube(
BufferDescription(ctx.buffer(data=normal), '3f', ['in_normal']),
BufferDescription(ctx.buffer(data=uv), '2f', ['in_uv']),
])


def sphere(
radius=0.5,
sectors=32,
rings=16,
normals=True,
uvs=True,
) -> Geometry:
"""
Creates a 3D sphere.

:param float radius: Radius or the sphere
:param int rings: number or horizontal rings
:param int sectors: number of vertical segments
:param bool normals: Include normals in the VAO
:param bool uvs: Include texture coordinates in the VAO
:return: A geometry object
"""
ctx = _get_active_context()

R = 1.0 / (rings - 1)
S = 1.0 / (sectors - 1)

vertices = [0] * (rings * sectors * 3)
normals = [0] * (rings * sectors * 3)
uvs = [0] * (rings * sectors * 2)

v, n, t = 0, 0, 0
for r in range(rings):
for s in range(sectors):
y = math.sin(-math.pi / 2 + math.pi * r * R)
x = math.cos(2 * math.pi * s * S) * math.sin(math.pi * r * R)
z = math.sin(2 * math.pi * s * S) * math.sin(math.pi * r * R)

uvs[t] = s * S
uvs[t + 1] = r * R

vertices[v] = x * radius
vertices[v + 1] = y * radius
vertices[v + 2] = z * radius

normals[n] = x
normals[n + 1] = y
normals[n + 2] = z

t += 2
v += 3
n += 3

indices = [0] * rings * sectors * 6
i = 0
for r in range(rings - 1):
for s in range(sectors - 1):
indices[i] = r * sectors + s
indices[i + 1] = (r + 1) * sectors + (s + 1)
indices[i + 2] = r * sectors + (s + 1)

indices[i + 3] = r * sectors + s
indices[i + 4] = (r + 1) * sectors + s
indices[i + 5] = (r + 1) * sectors + (s + 1)
i += 6

content = [
BufferDescription(ctx.buffer(data=array('f', vertices)), "3f", ["in_position"]),
]
if normals:
content.append(BufferDescription(ctx.buffer(data=array('f', normals)), "3f", ["in_normal"]))
if uvs:
content.append(BufferDescription(ctx.buffer(data=array('f', uvs)), "2f", ["in_uv"]))

return ctx.geometry(
content,
index_buffer=ctx.buffer(data=array('I', indices)),
mode=ctx.TRIANGLES,
)