SDL2.0 是一个跨平台的底层图形库,支持 Windows,Mac,Android,IOS 等多个平台,考虑到我们需要观察元宇宙是如何运作的,即使不考虑客户端,仅仅是元宇宙服务器,增加一个 SDL 的观察方法,也是有助于开发和调试的。
Rust 的 SDL2.0 绑定比较成熟,但是如果要显示文字的话,就比较坑了,SDL2.0 自己带了一个 SDL_TTF 的库,支持 FreeType + SDL2.0 显示文字,但是 依赖非常多,特别是在国内,Mac brew 下载 freetype 的库,简直就是噩梦。
所以我们选择 纯 rust 的 字体解决方案
支持 TrueType 和 OpenType 的 ttf-parser 以及渲染字体的 fontdue
使用 fontdue 基本获取 字体的代码 如下
let font = File::open(font_name).map(|mut file| {
let mut buf = Vec::<u8>::new();
file.read_to_end(&mut buf).unwrap();
buf
}).ok().and_then(|data| fontdue::Font::from_bytes(data, fontdue::FontSettings::default()).ok()).unwrap();
let mut size = (0, 0);
let metrics_bitmaps = text.chars().fold(Vec::new(), |mut v, ch| {
let mb = self.font.rasterize(ch, height);
if mb.0.advance_height as u32 > size.1 { size.1 = mb.0.advance_height as u32; }
size.0 += mb.0.advance_width as u32;
v.push(mb);
v
});
复制代码
其中 font_name 是字体文件的名字
text 是需要显示的字符串 String 或者 &str
获取的 (metrics, bitmap) 的 列表放在 一个 Vec 里面,size 是一个元组,存放了这行文字的 width 和 height,我们这里没有考虑换行的问题。
SDL2.0 基本的 窗口显示和消息处理流程如下
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem.window("元宇宙 - 观察者", 1280,1024).position_centered().build().unwrap();
let mut canvas = window.into_canvas().build().unwrap();
let mut event_pump = sdl_context.event_pump().unwrap();
'running: loop {
for event in event_pump.poll_iter() {
match event {
Event::Quit {..} |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
break 'running
},
_ => {}
}
}
canvas.present();
std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
}
复制代码
这里 我们要为语句标号 Label 和 goto 平反,类似这样 break 到 循环的开始,逻辑上是很清晰的,比完全不使用 goto + label 的方案要容易理解多了
SDL2.0 有多种绘制的方法,一种是直接在 Canvas 上面绘制,但是在多线程的环境下,就会出现 canvas.present 和 对 canvas 的绘制不在同一个线程,这样会造成程序运行很不流畅,另外,大部分需要显示的内容是跟具体的对象相关的,比如说 角色的 ID Position 或者名字,头像等等,这些内容显示的范围较小,而且更新不那么频繁,所以我们可以使用 一张很大的 Texture 里面的一部分,来存放显示的内容。
需要显示的时候,一个 copy 就能迅速地送到屏幕缓冲区中。
可划分的 Texture 作为缓存
我们使用 Block 表示 Texture 中的一个块(占用的区域),使用 Container 来处理 Texture 作为缓冲区的分配和释放,使用一个 vector 处理 一个 texture 不够的情况(绝大多数情况用不到)
#[derive(Debug, Clone)]
struct Block { x: u32, y: u32, w: u32, h: u32 }
impl Container {
fn new(w: u32, h: u32)-> Container {
Container{
w: w,
h: h,
free_blocks: vec![Block{x: 0, y: 0, w: w, h: h}],
used_blocks: Vec::new()
}
}
fn get(&self, id: u32)-> Block {
return self.used_blocks[id as usize].clone();
}
fn malloc(&mut self, w: u32, h: u32)-> i32 { //malloc
}
fn free(&mut self, id: i32) { //free a block
}
}
复制代码
Container 有两个基本方法,malloc 一个块,free 一个块
textures: Vec<(Container, Texture)>,
复制代码
我们使用 textures 数组,来存放所有的 Texture 和 它们的容器
下面的代码 在贴图中分配一个矩形块,ID 作为唯一标识,以后可以用来绘制和显示
fn get_id(&mut self, w: u32, h: u32)-> u32 {
for index in 0..self.textures.len() {
let id = self.textures[index].0.malloc(w, h);
if id >= 0 {
return (id as u32) | (index as u32) << 16;
}
}
let mut container = Container::new(TEXTURE_SIZE, TEXTURE_SIZE);
let id = container.malloc(w, h);
let index = self.textures.len() as u32;
let mut tex = self.creator.create_texture_target(PixelFormatEnum::RGBA32, TEXTURE_SIZE, TEXTURE_SIZE).unwrap();
tex.set_blend_mode(BlendMode::Blend);
self.textures.push((container, tex));
if id >= 0 { (id as u32) | index << 16 }
else { 0xffffffffu32 }
}
复制代码
下面的代码 在这个缓冲区里面进行绘制
fn draw<F: FnOnce(&mut Canvas<sdl2::video::Window>)>(&mut self, id: u32, f: F) {
let index = (id >> 16) as usize;
if index < self.textures.len() {
let rect: Rect = Rect::from(self.textures[index].0.get(id & 0xffff));
self.canvas.with_texture_canvas(&mut self.textures[index].1, |canvas| {
canvas.set_viewport(rect);
f(canvas);
}).unwrap();
}
}
复制代码
比如说:使用指定颜色和 alpha 清除一个缓冲区
self.draw(self.nodes[label].id, |canvas| {
canvas.set_draw_color(Color::RGBA(0x8f, 0x8f, 0x80, 0x40));
canvas.clear();
});
复制代码
使用前述获取的文字 metrics 和 bitmap,绘制指定颜色文字的代码如下
self.draw(id, |canvas| {
let mut pos = 0;
for mb in metrics_bitmaps {
let _top = y + mb.0.advance_height as i32 - mb.0.height as i32;
for top in 0..mb.0.height {
for left in 0..(mb.0.width) {
let gray = mb.1[top * mb.0.width + left];
if gray != 0 {
canvas.set_draw_color(Color::RGBA((color >> 16) as u8, ((color >> 8) & 0xff) as u8, (color & 0xff) as u8, gray));
canvas.draw_point((x + pos + left as i32, _top + top as i32)).unwrap();
}
}
}
pos += mb.0.advance_width as i32;
}
});
复制代码
评论