Skip to content

Commit 9103f90

Browse files
authored
1.3.2.0 Release
1 parent f84a653 commit 9103f90

1 file changed

Lines changed: 85 additions & 14 deletions

File tree

engine/terrain.py

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,23 @@ def build(self, height_func):
319319
wz = self.world_z + iz * self.step
320320
self.heights[ix, iz] = height_func(wx, wz)
321321
self.is_valid = True
322+
323+
def build_batch(self, height_func_batch):
324+
"""Vectorised equivalent of build(): evaluate the whole grid in one
325+
batched call instead of resolution*resolution scalar Python calls.
326+
The scalar path evaluates a few fbm/ridge octaves per point, so the
327+
pure-Python double loop dominates chunk upload time and stalls the
328+
render thread; the batched noise path is 1-2 orders of magnitude
329+
faster for the same result."""
330+
ix = np.arange(self.resolution, dtype=np.float32)
331+
iz = np.arange(self.resolution, dtype=np.float32)
332+
ixg, izg = np.meshgrid(ix, iz, indexing='ij')
333+
wx = (self.world_x + ixg * self.step).ravel()
334+
wz = (self.world_z + izg * self.step).ravel()
335+
heights = height_func_batch(wx, wz)
336+
self.heights = np.asarray(heights, dtype=np.float32).reshape(
337+
(self.resolution, self.resolution))
338+
self.is_valid = True
322339

323340
def get_height(self, world_x: float, world_z: float) -> Optional[float]:
324341
if not self.is_valid or self.heights is None:
@@ -417,6 +434,7 @@ def __init__(self, texture_manager=None, seed: int = 42):
417434
self.rock_tex = 0
418435
self.sand_tex = 0
419436
self.snow_tex = 0
437+
self._placeholder_cubemap = 0
420438
if texture_manager:
421439
self.load_terrain_textures(texture_manager)
422440
self._init_shader()
@@ -751,7 +769,7 @@ def _generate_chunk_mesh(self, chunk: TerrainChunk, resolution: int) -> np.ndarr
751769

752770
def _upload_chunk(self, chunk: TerrainChunk, resolution: int):
753771
if chunk.height_cache and not chunk.height_cache.is_valid:
754-
chunk.height_cache.build(self._get_height_scalar)
772+
chunk.height_cache.build_batch(self._get_heights_batch)
755773
vertex_data = self._generate_chunk_mesh(chunk, resolution)
756774
chunk.vertex_count = len(vertex_data) // 14
757775
if not chunk.vao:
@@ -795,18 +813,55 @@ def _is_chunk_visible(self, chunk: TerrainChunk, frustum_planes) -> bool:
795813
if a * px + b * py + c * pz + d < 0: return False
796814
return True
797815

816+
def _ensure_placeholder_cubemap(self) -> int:
817+
"""Lazily create a 1x1 complete cube-map used for shadow sampler units
818+
that have no real depth cube-map (shadows off, or fewer cube-maps than
819+
sampler slots). A complete texture on every unit guarantees no
820+
samplerCube is left referencing texture unit 0 or an incomplete texture,
821+
either of which can make glDrawArrays raise GL_INVALID_OPERATION."""
822+
if self._placeholder_cubemap:
823+
return self._placeholder_cubemap
824+
tex = int(gl.glGenTextures(1))
825+
gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, tex)
826+
black = (ctypes.c_ubyte * 4)(0, 0, 0, 255)
827+
for face in range(6):
828+
gl.glTexImage2D(gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, gl.GL_RGBA,
829+
1, 1, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, black)
830+
gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
831+
gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
832+
gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
833+
gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
834+
gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE)
835+
gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, 0)
836+
self._placeholder_cubemap = tex
837+
return self._placeholder_cubemap
838+
798839
def update_and_render(self, projection: glm.mat4, view: glm.mat4, camera_pos: glm.vec3, frustum_planes=None, lights=None, active_lights_count=0,
799840
shadow_cubemaps=None, shadow_index_map=None, shadow_unit_base=4):
800841
if not self.enabled: return
801842
if not self.shader_program:
802843
self._init_shader()
803844
if not self.shader_program: return
804845

805-
# Ensure late-bound uniforms exist (can happen if shader was set externally)
806-
if 'use_textures' not in self.uniforms:
807-
self.uniforms['use_textures'] = gl.glGetUniformLocation(self.shader_program, 'use_textures')
808-
if 'lod_level' not in self.uniforms:
809-
self.uniforms['lod_level'] = gl.glGetUniformLocation(self.shader_program, 'lod_level')
846+
# Re-resolve late-bound uniforms against the *current* program. The
847+
# renderer can swap in an externally-compiled program whose uniform
848+
# table only covers a subset of locations (see
849+
# Renderer.setup_terrain_shader), so any sampler we rely on must be
850+
# looked up here or it stays unbound. An unassigned ``samplerCube``
851+
# defaults to texture unit 0, collides with the ``sampler2D`` terrain
852+
# textures bound there, and makes glDrawArrays raise
853+
# GL_INVALID_OPERATION.
854+
for name in ('use_textures', 'lod_level'):
855+
if name not in self.uniforms:
856+
self.uniforms[name] = gl.glGetUniformLocation(self.shader_program, name)
857+
for i in range(8):
858+
key = f'lights[{i}].shadowIndex'
859+
if key not in self.uniforms:
860+
self.uniforms[key] = gl.glGetUniformLocation(self.shader_program, key)
861+
for i in range(shaders.MAX_SHADOW_LIGHTS):
862+
key = f'shadowMaps[{i}]'
863+
if key not in self.uniforms:
864+
self.uniforms[key] = gl.glGetUniformLocation(self.shader_program, key)
810865

811866
self.visible_chunks = 0
812867
self.culled_chunks = 0
@@ -846,14 +901,30 @@ def update_and_render(self, projection: glm.mat4, view: glm.mat4, camera_pos: gl
846901
gl.glUniform1i(sidx_loc, shadow_index_map.get(id(light), -1))
847902

848903
# Bind depth cube-maps so terrain receives point-light shadows.
849-
if shadow_cubemaps:
850-
for i, cm in enumerate(shadow_cubemaps):
851-
loc = self.uniforms.get(f'shadowMaps[{i}]', -1)
852-
if loc is not None and loc != -1:
853-
gl.glActiveTexture(gl.GL_TEXTURE0 + shadow_unit_base + i)
854-
gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, cm)
855-
gl.glUniform1i(loc, shadow_unit_base + i)
856-
gl.glActiveTexture(gl.GL_TEXTURE0)
904+
#
905+
# Every ``samplerCube shadowMaps[i]`` uniform must be pointed at its own
906+
# reserved texture unit *unconditionally*. GLSL samplers default to
907+
# texture unit 0, which already holds a ``sampler2D`` (texGrass). The
908+
# spec forbids two different sampler types referencing the same texture
909+
# image unit, so leaving the cube samplers on unit 0 makes the driver
910+
# raise GL_INVALID_OPERATION on the very next draw call. We therefore
911+
# always assign the units and bind a *complete* cube-map to each — the
912+
# real depth cube-map when available, otherwise a 1x1 placeholder — even
913+
# when shadows are disabled or fewer cube-maps than sampler slots are
914+
# supplied. Binding the incomplete default texture (name 0) would leave
915+
# an active samplerCube pointing at an incomplete texture, which some
916+
# drivers also reject at draw time.
917+
shadow_cubemaps = shadow_cubemaps or []
918+
placeholder = self._ensure_placeholder_cubemap()
919+
for i in range(shaders.MAX_SHADOW_LIGHTS):
920+
loc = self.uniforms.get(f'shadowMaps[{i}]', -1)
921+
if loc is None or loc == -1:
922+
continue
923+
cm = shadow_cubemaps[i] if (i < len(shadow_cubemaps) and shadow_cubemaps[i]) else placeholder
924+
gl.glActiveTexture(gl.GL_TEXTURE0 + shadow_unit_base + i)
925+
gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, cm)
926+
gl.glUniform1i(loc, shadow_unit_base + i)
927+
gl.glActiveTexture(gl.GL_TEXTURE0)
857928

858929
lod_level_loc = self.uniforms.get('lod_level', -1)
859930

0 commit comments

Comments
 (0)