1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
"""
Quick and dirty example showing how sprites can be rendered using a geometry shader.
We also show simple scrolling with projection matrix.
The goal is to redice the sprite data on the client as much as possible.
We can define a sprite by its position, size and rotation. This can be
expressed in 5 32 bit floats. This also opens up for individually rotating
each sprite in the shader itself. This technique can be extended with more
sprite parameters.
Other optimizations that can be done:
* Cull sprites outside the viewport in geo shader
"""
import math
import moderngl
from _example import Example
from pyrr import Matrix44
from array import array
class GeoSprites(Example):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.ball_texture = self.load_texture_2d("ball.png")
# Sprite shader using geometry shader
self.program = self.ctx.program(
vertex_shader="""
#version 330
// The per sprite input data
in vec2 in_position;
in vec2 in_size;
in float in_rotation;
out vec2 size;
out float rotation;
void main() {
// We just pass the values unmodified to the geometry shader
gl_Position = vec4(in_position, 0, 1);
size = in_size;
rotation = in_rotation;
}
""",
geometry_shader="""
#version 330
// We are taking single points form the vertex shader
// and emitting 4 new vertices creating a quad/sprites
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
uniform mat4 projection;
// Since geometry shader can take multiple values from a vertex
// shader we need to define the inputs from it as arrays.
// In our instance we just take single values (points)
in vec2 size[];
in float rotation[];
out vec2 uv;
void main() {
// We grab the position value from the vertex shader
vec2 center = gl_in[0].gl_Position.xy;
// Calculate the half size of the sprites for easier calculations
vec2 hsize = size[0] / 2.0;
// Convert the rotation to radians
float angle = radians(rotation[0]);
// Create a 2d rotation matrix
mat2 rot = mat2(
cos(angle), sin(angle),
-sin(angle), cos(angle)
);
// Emit a triangle strip creating a quad (4 vertices).
// Here we need to make sure the rotation is applied before we position the sprite.
// We just use hardcoded texture coordinates here. If an atlas is used we
// can pass an additional vec4 for specific texture coordinates.
// Each EmitVertex() emits values down the shader pipeline just like a single
// run of a vertex shader, but in geomtry shaders we can do it multiple times!
// Upper left
gl_Position = projection * vec4(rot * vec2(-hsize.x, hsize.y) + center, 0.0, 1.0);
uv = vec2(0, 1);
EmitVertex();
// lower left
gl_Position = projection * vec4(rot * vec2(-hsize.x, -hsize.y) + center, 0.0, 1.0);
uv = vec2(0, 0);
EmitVertex();
// upper right
gl_Position = projection * vec4(rot * vec2(hsize.x, hsize.y) + center, 0.0, 1.0);
uv = vec2(1, 1);
EmitVertex();
// lower right
gl_Position = projection * vec4(rot * vec2(hsize.x, -hsize.y) + center, 0.0, 1.0);
uv = vec2(1, 0);
EmitVertex();
// We are done with this triangle strip now
EndPrimitive();
}
""",
fragment_shader="""
#version 330
uniform sampler2D sprite_texture;
in vec2 uv;
out vec4 fragColor;
void main() {
fragColor = texture(sprite_texture, uv);
}
""",
)
self.sprite_data_size = 5 * 4 # 5 32 bit floats
self.sprite_data = self.ctx.buffer(reserve=1000 * self.sprite_data_size) # Capacity for 1000 sprites
self.vao = self.ctx.vertex_array(
self.program,
[
(self.sprite_data, "2f 2f 1f", "in_position", "in_size", "in_rotation"),
]
)
def render(self, time, frame_time):
self.ctx.clear()
self.ctx.enable(moderngl.BLEND)
num_sprites = 16
# We'll just generate some sprite data on the fly here.
# This should only be necessary every time the sprite data changes (in a prefect wold)
# Grab the size of the screen or current render target
width, height = self.ctx.fbo.size
# We just create a generator function instead of
def gen_sprites(time):
rot_step = math.pi * 2 / num_sprites
for i in range(num_sprites):
# Position
yield width / 2 + math.sin(time + rot_step * i) * 300
yield height / 2 + math.cos(time + rot_step * i) * 300
# size
yield 100
yield 100
# rotation
yield math.sin(time + i) * 200
self.sprite_data.write(array("f", gen_sprites(time)))
# calculate scroll offsets. We truncate to integers here.
# This depends what "look" you want for your game.
scroll_x, scroll_y = int(math.sin(time) * 100), int(math.cos(time) * 100)
# Create a orthogonal projection matrix
# Let's also modify the projection to scroll the entire screen.
projection = Matrix44.orthogonal_projection(
scroll_x, # left
width + scroll_x, # right
height + scroll_y, # top
scroll_y, # bottom
1, # near
-1, # far
dtype="f4", # ensure we create 32 bit value (64 bit is default)
)
# Update the projection uniform
self.program["projection"].write(projection)
# Configure the sprite_texture uniform to read from texture channel 0
self.program["sprite_texture"] = 0
# Bind the texture to channel 0
self.ball_texture.use(0)
# Since we have overallocated the buffer (room for 1000 sprites) we
# need to specify how many we actually want to render passing number of vertices.
# Also the mode needs to be the same as the geometry shader input type (points!)
self.vao.render(mode=moderngl.POINTS, vertices=num_sprites)
if __name__ == "__main__":
GeoSprites.run()
|