Python Ursina引擎大量实体渲染优化:减少延迟技巧
2025-03-17 10:56:48
Python Ursina 引擎中优化大量实体渲染,减少延迟
这问题挺常见的, Ursina 里一下子加载一大堆实体, 特别是像 Minecraft 那种, 很容易卡。做游戏嘛, 谁不想帧率高点呢?先说下你碰到的情况, 建了个 25x25 的世界,铺了 3 层方块,就有 1900 个方块了。现在就觉得卡, 以后方块多了肯定更卡。你想问能不能远处的实体不渲染, 或者暂时禁用, 等靠近了再显示。
问题分析:为什么会卡?
渲染过程吃性能. Ursina (或者说任何 3D 引擎)渲染物体,都需要做很多计算。这些计算包括:
- 顶点处理: 每个方块都有顶点,引擎要知道这些顶点在屏幕上的位置。
- 光栅化: 把 3D 物体变成屏幕上的像素。
- 纹理映射: 把图片(纹理)贴到物体表面。
- 光照计算: 计算光线对物体的影响。
方块越多,这些计算量越大。即便是做了视锥体裁剪(只渲染视野内的物体),如果视野内方块依然很多,帧率还是上不去。
解决方案:
咱们来逐个试试解决办法.
1. 距离判断禁用 (效果有限,但可改进)
你已经试过这个了, 就是算每个方块和玩家的距离, 太远了就 disable()
。想法是对的, 但是直接这么搞, 反而更卡. 为啥呢? 因为你每帧都要算所有方块的距离! 这计算量也很大.
改进方案:
- 降低检查频率: 不用每帧都检查,可以隔几帧,甚至隔几秒检查一次。隔多久可以根据实际情况调,找到一个平衡点。
- 分组管理: 把世界分成大的区块 (Chunks), 比如 16x16x16 大小的。只检查区块和玩家的距离, 如果区块远了, 把整个区块的方块都禁用。
这样可以大大减少距离计算次数。 - 分层处理:把游戏分成多个不同详细程度的模型,然后不同距离使用不同的模型。比如,在几千米的地方是一个简单几何模型,而几百米内的显示完整模型。
示例代码 (降低检查频率):
from ursina import *
app = Ursina()
# 假设的玩家和方块列表
player = Entity(model='cube', color=color.orange)
blocks = [Entity(model='cube', position=(x, y, z)) for x in range(25) for y in range(3) for z in range(25)]
check_interval = 30 # 每30帧检查一次
frame_count = 0
def update():
global frame_count
frame_count += 1
if frame_count >= check_interval:
frame_count = 0
for block in blocks:
if distance(player, block) > 10:
block.disable()
else:
block.enable()
#下面是让玩家可以移动
if held_keys['a']:
player.x -= time.dt * 5
if held_keys['d']:
player.x += time.dt * 5
if held_keys['w']:
player.z += time.dt * 5
if held_keys['s']:
player.z -= time.dt * 5
if held_keys['space']:
player.y += time.dt * 5
if held_keys['shift']:
player.y -= time.dt * 5
EditorCamera()
app.run()
示例代码 (区块管理):
from ursina import *
app = Ursina()
player = Entity(model='cube', color=color.orange)
blocks = []
chunk_size = 8
#生成8x3x8的世界, 每个区块为 8x8x8.
world_size_x = 8
world_size_y = 3
world_size_z = 8
chunks = {} # 用字典存储区块, key 是区块的坐标 (x, y, z)
def create_chunk(x, y, z):
chunk = []
for cx in range(chunk_size):
for cy in range(chunk_size):
for cz in range(chunk_size):
block = Entity(model='cube', position=(x + cx, y + cy, z + cz))
chunk.append(block)
chunks[(x, y, z)] = chunk
# 创建所有区块
for x in range(0,world_size_x*chunk_size,chunk_size):
for y in range(0, world_size_y * chunk_size, chunk_size):
for z in range(0,world_size_z*chunk_size,chunk_size):
create_chunk(x, y, z)
check_interval = 60
frame_count = 0
def update():
global frame_count
frame_count += 1
if frame_count >= check_interval:
frame_count = 0
for chunk_pos, chunk_blocks in chunks.items():
chunk_center = Vec3(chunk_pos[0] + chunk_size / 2, chunk_pos[1] + chunk_size / 2, chunk_pos[2] + chunk_size / 2)
if distance(player.position, chunk_center) > 20: # 调整这个距离
for block in chunk_blocks:
block.disable()
else:
for block in chunk_blocks:
block.enable()
#控制移动
if held_keys['a']:
player.x -= time.dt * 5
if held_keys['d']:
player.x += time.dt * 5
if held_keys['w']:
player.z += time.dt * 5
if held_keys['s']:
player.z -= time.dt * 5
if held_keys['space']:
player.y += time.dt * 5
if held_keys['shift']:
player.y -= time.dt * 5
EditorCamera()
app.run()
2. 使用 combine()
合并网格
你说不能把网格拆回方块, 其实 combine()
方法有一些参数是可以控制合并后的行为的. 关键是 auto_destroy
和 keep_origin
这两个参数.
原理:
combine()
: 把多个实体的网格合并成一个,减少 draw call (绘制调用) 次数。auto_destroy=False
: 合并后,原来的实体不会被销毁, 还保留着.keep_origin=True
: 保持合并后网格的原点位置,方便后续操作.
步骤:
- 创建方块时,先正常创建。
- 用
combine()
把一个区域内的方块 (比如一个 Chunk) 合并成一个大网格,auto_destroy
设为False
,keep_origin
设为True
。 - 要破坏方块时,找到对应的原始方块实体 (因为
auto_destroy=False
,它们还在),把它从场景中移除。 - 重新
combine()
受影响区域的方块.
注意点:
需要自己记录每个方块和合并后的大网格的对应关系,才能在破坏时找到正确的方块。可以用字典来存储。
示例代码 (简化版,演示 combine 的用法):
from ursina import *
app = Ursina()
# 创建一些方块
blocks = []
for x in range(5):
for y in range(3):
for z in range(5):
block = Entity(model='cube', position=(x, y, z))
blocks.append(block)
# 合并方块
combined_mesh = Entity(model='cube')
combined_mesh.combine(blocks, auto_destroy=False, keep_origin=True)
# 破坏其中一个方块 (假设是坐标 (1, 1, 1) 的)
for b in blocks:
if b.position == (1,1,1):
b.disable()#这里也可以使用b.parent = None来将其暂时不挂载
#重新combine
combined_mesh2 = Entity(model='cube')
combined_mesh2.combine(blocks, auto_destroy=False, keep_origin=True)
combined_mesh.disable()#可以关掉旧的, 可以留到之后复用,减少对象创建的消耗.
EditorCamera()
app.run()
3. 实例化渲染 (Instancing)
这个厉害了! 如果你的方块长得都一样 (比如都是普通立方体), 强烈推荐用实例化.
原理:
实例化是一种特殊的渲染技术。 告诉显卡:"我要画 N 个长得一样的物体, 你只算一次, 然后复制 N 份就行了." 这样可以极大减少 CPU 和 GPU 的负担.
Ursina 中使用:
Ursina 封装了实例化, 用起来很简单. 就是把模型换成 'instanced_cube'
。
from ursina import *
app = Ursina()
# 注意这里模型是 'instanced_cube'
blocks = [Entity(model='instanced_cube', position=(x, y, z)) for x in range(25) for y in range(3) for z in range(25)]
camera.clip_plane_far = 25
player = Entity(model='cube', color = color.orange)
def input(key):
if key == 'q':
print("改变")
for block in blocks:
if block.position.y <= 1:
block.color = color.random_color()
#重新进行渲染
Entity(model='instanced_cube').combine(blocks, auto_destroy=True)
EditorCamera()
app.run()
注意:
实例化渲染的物体, 修改颜色、纹理之类的属性会比较麻烦, 因为它们共享一份数据. 如果你的方块有不同颜色或纹理,需要一些额外的技巧.
实例化后的combine,修改颜色或者纹理需要重新执行一遍Entity(model='instanced_cube').combine(blocks, auto_destroy=True),否则会不生效. 这种方法的局限性较大。不适用于频繁修改颜色的方块.
进阶:自定义 Instanced 模型
可以不使用'instanced_cube'
,使用其他自定义的模型.
- 你需要用
Mesh()
类创建一个自定义网格。 - 在创建实体时, 使用
add_instance(position)
方法添加实例,而不是直接指定position
。
这样做的灵活度很高,你几乎可以用任何形状的模型做实例化渲染.
4. 使用着色器 (Shaders)
如果上面这些方法都不能满足你, 并且你对图形学比较了解, 还可以考虑用着色器。
着色器是直接跑在 GPU 上的小程序. 可以自己写着色器来实现一些特殊的渲染效果, 比如把很多方块的顶点数据一次性传给 GPU, 在着色器里计算每个方块的位置, 这样可以把大量的计算从 CPU 转移到 GPU.
但是注意了,这个门槛很高, 需要学习着色器语言 (比如 GLSL). Ursina 支持自定义着色器, 但这属于高级技巧, 这里就不展开讲了.
5. camera.clip_plane_far
的补充
你设置了 camera.clip_plane_far = 10
, 这是限制了渲染距离.
如果方块真的很少的话,可以增加一点, 或者改为通过代码控制,这样给玩家更多控制的余地.
总结:
选哪个方法,要根据你具体的需求和技术水平来定:
- 简单场景: 分组 + 降低检查频率 + 适当调整
camera.clip_plane_far
应该够用了. - 方块都一样: 强烈推荐实例化渲染! 性能提升巨大。
- 需要破坏/修改方块:
combine()
结合分组管理, 自己实现方块的移除和更新。 - 追求极致性能: 了解一下着色器。
上面提到的所有方法,建议你在项目里多尝试一下, 调一调参数, 才能找到最优解.