1044 lines
35 KiB
Rust
1044 lines
35 KiB
Rust
//! The `graphics` module performs the actual drawing of images, text, and other
|
|
//! objects with the [`Drawable`](trait.Drawable.html) trait. It also handles
|
|
//! basic loading of images and text.
|
|
//!
|
|
//! This module also manages graphics state, coordinate systems, etc.
|
|
//! The default coordinate system has the origin in the upper-left
|
|
//! corner of the screen, with Y increasing downwards.
|
|
//!
|
|
//! This library differs significantly in performance characteristics from the
|
|
//! `LÖVE` library that it is based on. Many operations that are batched by default
|
|
//! in love (e.g. drawing primitives like rectangles or circles) are *not* batched
|
|
//! in `ggez`, so render loops with a large number of draw calls can be very slow.
|
|
//! The primary solution to efficiently rendering a large number of primitives is
|
|
//! a [`SpriteBatch`](spritebatch/struct.SpriteBatch.html), which can be orders
|
|
//! of magnitude more efficient than individual
|
|
//! draw calls.
|
|
//!
|
|
//! The `pipe` module is auto-generated by `gfx_defines!`. You shouldn't need to
|
|
//! touch it, but alas we can't exclude it from `cargo doc`.
|
|
|
|
use std::collections::HashMap;
|
|
use std::convert::From;
|
|
use std::fmt;
|
|
use std::path::Path;
|
|
use std::u16;
|
|
|
|
use gfx;
|
|
use gfx::Device;
|
|
use gfx::Factory;
|
|
use gfx::texture;
|
|
use gfx_device_gl;
|
|
use glutin;
|
|
use glutin::{NotCurrent, PossiblyCurrent};
|
|
pub use mint;
|
|
pub(crate) use nalgebra as na;
|
|
use winit::event_loop::EventLoopWindowTarget;
|
|
|
|
use glutin_ext::*;
|
|
|
|
use crate::ggez::conf;
|
|
use crate::ggez::conf::WindowMode;
|
|
use crate::ggez::context::Context;
|
|
use crate::ggez::context::DebugId;
|
|
use crate::ggez::GameError;
|
|
use crate::ggez::GameResult;
|
|
pub use crate::ggez::graphics::canvas::*;
|
|
pub use crate::ggez::graphics::drawparam::*;
|
|
pub use crate::ggez::graphics::image::*;
|
|
pub use crate::ggez::graphics::mesh::*;
|
|
pub use crate::ggez::graphics::shader::*;
|
|
pub use crate::ggez::graphics::types::*;
|
|
|
|
pub(crate) mod canvas;
|
|
pub(crate) mod context;
|
|
pub(crate) mod drawparam;
|
|
pub(crate) mod glutin_ext;
|
|
pub(crate) mod image;
|
|
pub(crate) mod mesh;
|
|
pub(crate) mod shader;
|
|
pub(crate) mod types;
|
|
|
|
pub mod spritebatch;
|
|
|
|
// This isn't really particularly nice, but it's only used
|
|
// in a couple places and it's not very easy to change or configure.
|
|
// Since the next major project is "rewrite the graphics engine" I think
|
|
// we're fine just leaving it.
|
|
//
|
|
// It exists basically because gfx-rs is incomplete and we can't *always*
|
|
// specify texture formats and such entirely at runtime, which we need to
|
|
// do to make sRGB handling work properly.
|
|
pub(crate) type BuggoSurfaceFormat = gfx::format::Rgba8;
|
|
type ShaderResourceType = [f32; 4];
|
|
|
|
type ColorFormat = gfx::format::Rgba8;
|
|
type DepthFormat = gfx::format::DepthStencil;
|
|
|
|
/// A trait providing methods for working with a particular backend, such as OpenGL,
|
|
/// with associated gfx-rs types for that backend. As a user you probably
|
|
/// don't need to touch this unless you want to write a new graphics backend
|
|
/// for ggez. (Trust me, you don't.)
|
|
pub trait BackendSpec: fmt::Debug {
|
|
/// gfx resource type
|
|
type Resources: gfx::Resources;
|
|
/// gfx factory type
|
|
type Factory: gfx::Factory<Self::Resources> + Clone;
|
|
/// gfx command buffer type
|
|
type CommandBuffer: gfx::CommandBuffer<Self::Resources>;
|
|
/// gfx device type
|
|
type Device: gfx::Device<Resources=Self::Resources, CommandBuffer=Self::CommandBuffer>;
|
|
|
|
/// A helper function to take a RawShaderResourceView and turn it into a typed one based on
|
|
/// the surface type defined in a `BackendSpec`.
|
|
///
|
|
/// But right now we only allow surfaces that use [f32;4] colors, so we can freely
|
|
/// hardcode this in the `ShaderResourceType` type.
|
|
fn raw_to_typed_shader_resource(
|
|
&self,
|
|
texture_view: gfx::handle::RawShaderResourceView<Self::Resources>,
|
|
) -> gfx::handle::ShaderResourceView<<Self as BackendSpec>::Resources, ShaderResourceType> {
|
|
// gfx::memory::Typed is UNDOCUMENTED, aiee!
|
|
// However there doesn't seem to be an official way to turn a raw tex/view into a typed
|
|
// one; this API oversight would probably get fixed, except gfx is moving to a new
|
|
// API model. So, that also fortunately means that undocumented features like this
|
|
// probably won't go away on pre-ll gfx...
|
|
let typed_view: gfx::handle::ShaderResourceView<_, ShaderResourceType> =
|
|
gfx::memory::Typed::new(texture_view);
|
|
typed_view
|
|
}
|
|
|
|
/// Returns the version of the backend, `(major, minor)`.
|
|
///
|
|
/// So for instance if the backend is using OpenGL version 3.2,
|
|
/// it would return `(3, 2)`.
|
|
fn version_tuple(&self) -> (u8, u8);
|
|
|
|
/// Returns the glutin `Api` enum for this backend.
|
|
fn api(&self) -> glutin::Api;
|
|
|
|
/// Returns the text of the vertex and fragment shader files
|
|
/// to create default shaders for this backend.
|
|
fn shaders(&self) -> (&'static [u8], &'static [u8]);
|
|
|
|
/// Returns a string containing some backend-dependent info.
|
|
fn info(&self, device: &Self::Device) -> String;
|
|
|
|
/// Creates the window.
|
|
fn init<'a>(
|
|
&self,
|
|
window_builder: glutin::window::WindowBuilder,
|
|
gl_builder: glutin::ContextBuilder<'a, NotCurrent>,
|
|
events_loop: &winit::event_loop::EventLoopWindowTarget<()>,
|
|
color_format: gfx::format::Format,
|
|
depth_format: gfx::format::Format,
|
|
) -> Result<
|
|
(
|
|
glutin::WindowedContext<PossiblyCurrent>,
|
|
Self::Device,
|
|
Self::Factory,
|
|
gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
),
|
|
glutin::CreationError,
|
|
>;
|
|
|
|
/// Create an Encoder for the backend.
|
|
fn encoder(factory: &mut Self::Factory) -> gfx::Encoder<Self::Resources, Self::CommandBuffer>;
|
|
|
|
/// Resizes the viewport for the backend. (right now assumes a Glutin window...)
|
|
fn resize_viewport(
|
|
&self,
|
|
color_view: &gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
depth_view: &gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
color_format: gfx::format::Format,
|
|
depth_format: gfx::format::Format,
|
|
window: &glutin::WindowedContext<PossiblyCurrent>,
|
|
) -> Option<(
|
|
gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
)>;
|
|
}
|
|
|
|
/// A backend specification for OpenGL.
|
|
/// This is different from [`Backend`](../conf/enum.Backend.html)
|
|
/// because this needs to be its own struct to implement traits
|
|
/// upon, and because there may need to be a layer of translation
|
|
/// between what the user asks for in the config, and what the
|
|
/// graphics backend code actually gets from the driver.
|
|
///
|
|
/// You shouldn't normally need to fiddle with this directly
|
|
/// but it has to be public because generic types like
|
|
/// [`Shader`](type.Shader.html) depend on it.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, SmartDefault)]
|
|
pub struct GlBackendSpec {
|
|
#[default = 3]
|
|
major: u8,
|
|
#[default = 2]
|
|
minor: u8,
|
|
#[default(glutin::Api::OpenGl)]
|
|
api: glutin::Api,
|
|
}
|
|
|
|
impl From<conf::Backend> for GlBackendSpec {
|
|
fn from(c: conf::Backend) -> Self {
|
|
match c {
|
|
conf::Backend::OpenGL { major, minor } => Self {
|
|
major,
|
|
minor,
|
|
api: glutin::Api::OpenGl,
|
|
},
|
|
conf::Backend::OpenGLES { major, minor } => Self {
|
|
major,
|
|
minor,
|
|
api: glutin::Api::OpenGlEs,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BackendSpec for GlBackendSpec {
|
|
type Resources = gfx_device_gl::Resources;
|
|
type Factory = gfx_device_gl::Factory;
|
|
type CommandBuffer = gfx_device_gl::CommandBuffer;
|
|
type Device = gfx_device_gl::Device;
|
|
|
|
fn version_tuple(&self) -> (u8, u8) {
|
|
(self.major, self.minor)
|
|
}
|
|
|
|
fn api(&self) -> glutin::Api {
|
|
self.api
|
|
}
|
|
|
|
fn shaders(&self) -> (&'static [u8], &'static [u8]) {
|
|
match self.api {
|
|
glutin::Api::OpenGl => (
|
|
include_bytes!("shader/basic_150.vert.glsl"),
|
|
include_bytes!("shader/basic_150.frag.glsl"),
|
|
),
|
|
glutin::Api::OpenGlEs => (
|
|
include_bytes!("shader/basic_es100.vert.glsl"),
|
|
include_bytes!("shader/basic_es100.frag.glsl"),
|
|
),
|
|
a => panic!("Unsupported API: {:?}, should never happen", a),
|
|
}
|
|
}
|
|
|
|
fn init<'a>(
|
|
&self,
|
|
window_builder: glutin::window::WindowBuilder,
|
|
gl_builder: glutin::ContextBuilder<'a, NotCurrent>,
|
|
events_loop: &EventLoopWindowTarget<()>,
|
|
color_format: gfx::format::Format,
|
|
depth_format: gfx::format::Format,
|
|
) -> Result<
|
|
(
|
|
glutin::WindowedContext<PossiblyCurrent>,
|
|
Self::Device,
|
|
Self::Factory,
|
|
gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
),
|
|
glutin::CreationError,
|
|
> {
|
|
Ok(gl_builder
|
|
.with_gfx_color_depth::<ColorFormat, DepthFormat>()
|
|
.build_windowed(window_builder, &events_loop)?
|
|
.init_gfx_raw(color_format, depth_format))
|
|
}
|
|
|
|
fn info(&self, device: &Self::Device) -> String {
|
|
let info = device.get_info();
|
|
format!(
|
|
"Driver vendor: {}, renderer {}, version {:?}, shading language {:?}",
|
|
info.platform_name.vendor,
|
|
info.platform_name.renderer,
|
|
info.version,
|
|
info.shading_language
|
|
)
|
|
}
|
|
|
|
fn encoder(factory: &mut Self::Factory) -> gfx::Encoder<Self::Resources, Self::CommandBuffer> {
|
|
factory.create_command_buffer().into()
|
|
}
|
|
|
|
fn resize_viewport(
|
|
&self,
|
|
color_view: &gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
depth_view: &gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
color_format: gfx::format::Format,
|
|
depth_format: gfx::format::Format,
|
|
window: &glutin::WindowedContext<PossiblyCurrent>,
|
|
) -> Option<(
|
|
gfx::handle::RawRenderTargetView<Self::Resources>,
|
|
gfx::handle::RawDepthStencilView<Self::Resources>,
|
|
)> {
|
|
// Basically taken from the definition of
|
|
// gfx_window_glutin::update_views()
|
|
let dim = color_view.get_dimensions();
|
|
assert_eq!(dim, depth_view.get_dimensions());
|
|
if let Some((cv, dv)) = window.updated_views_raw(dim, color_format, depth_format)
|
|
{
|
|
Some((cv, dv))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
const QUAD_VERTS: [Vertex; 4] = [
|
|
Vertex {
|
|
pos: [0.0, 0.0],
|
|
uv: [0.0, 0.0],
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
},
|
|
Vertex {
|
|
pos: [1.0, 0.0],
|
|
uv: [1.0, 0.0],
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
},
|
|
Vertex {
|
|
pos: [1.0, 1.0],
|
|
uv: [1.0, 1.0],
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
},
|
|
Vertex {
|
|
pos: [0.0, 1.0],
|
|
uv: [0.0, 1.0],
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
},
|
|
];
|
|
|
|
const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];
|
|
|
|
gfx_defines! {
|
|
/// Structure containing fundamental vertex data.
|
|
vertex Vertex {
|
|
pos: [f32; 2] = "a_Pos",
|
|
uv: [f32; 2] = "a_Uv",
|
|
color: [f32;4] = "a_VertColor",
|
|
}
|
|
|
|
/// Internal structure containing values that are different for each
|
|
/// drawable object. This is the per-object data that
|
|
/// gets fed into the shaders.
|
|
vertex InstanceProperties {
|
|
// the columns here are for the transform matrix;
|
|
// you can't shove a full matrix into an instance
|
|
// buffer, annoyingly.
|
|
col1: [f32; 4] = "a_TCol1",
|
|
col2: [f32; 4] = "a_TCol2",
|
|
col3: [f32; 4] = "a_TCol3",
|
|
col4: [f32; 4] = "a_TCol4",
|
|
src: [f32; 4] = "a_Src",
|
|
color: [f32; 4] = "a_Color",
|
|
}
|
|
|
|
/// Internal structure containing global shader state.
|
|
constant Globals {
|
|
mvp_matrix: [[f32; 4]; 4] = "u_MVP",
|
|
}
|
|
|
|
// Internal structure containing graphics pipeline state.
|
|
// This can't be a doc comment though because it somehow
|
|
// breaks the gfx_defines! macro though. :-(
|
|
pipeline pipe {
|
|
vbuf: gfx::VertexBuffer<Vertex> = (),
|
|
mvp: gfx::Global<[[f32; 4]; 4]> = "u_MVP",
|
|
tex: gfx::TextureSampler<[f32; 4]> = "t_Texture",
|
|
globals: gfx::ConstantBuffer<Globals> = "Globals",
|
|
rect_instance_properties: gfx::InstanceBuffer<InstanceProperties> = (),
|
|
// The default values here are overwritten by the
|
|
// pipeline init values in `shader::create_shader()`.
|
|
out: gfx::RawRenderTarget =
|
|
("Target0",
|
|
gfx::format::Format(gfx::format::SurfaceType::R8_G8_B8_A8, gfx::format::ChannelType::Unorm),
|
|
gfx::state::ColorMask::all(), Some(gfx::preset::blend::ALPHA)
|
|
),
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for InstanceProperties {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut matrix_vec: Vec<f32> = vec![];
|
|
matrix_vec.extend(&self.col1);
|
|
matrix_vec.extend(&self.col2);
|
|
matrix_vec.extend(&self.col3);
|
|
matrix_vec.extend(&self.col4);
|
|
let matrix = na::Matrix4::from_column_slice(&matrix_vec);
|
|
writeln!(
|
|
f,
|
|
"Src: ({},{}+{},{})",
|
|
self.src[0], self.src[1], self.src[2], self.src[3]
|
|
)?;
|
|
writeln!(f, "Color: {:?}", self.color)?;
|
|
write!(f, "Matrix: {}", matrix)
|
|
}
|
|
}
|
|
|
|
impl Default for InstanceProperties {
|
|
fn default() -> Self {
|
|
InstanceProperties {
|
|
col1: [1.0, 0.0, 0.0, 0.0],
|
|
col2: [0.0, 1.0, 0.0, 0.0],
|
|
col3: [1.0, 0.0, 1.0, 0.0],
|
|
col4: [1.0, 0.0, 0.0, 1.0],
|
|
src: [0.0, 0.0, 1.0, 1.0],
|
|
color: [1.0, 1.0, 1.0, 1.0],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A structure for conveniently storing `Sampler`'s, based off
|
|
/// their `SamplerInfo`.
|
|
pub(crate) struct SamplerCache<B>
|
|
where
|
|
B: BackendSpec,
|
|
{
|
|
samplers: HashMap<texture::SamplerInfo, gfx::handle::Sampler<B::Resources>>,
|
|
}
|
|
|
|
impl<B> SamplerCache<B>
|
|
where
|
|
B: BackendSpec,
|
|
{
|
|
fn new() -> Self {
|
|
SamplerCache {
|
|
samplers: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn get_or_insert(
|
|
&mut self,
|
|
info: texture::SamplerInfo,
|
|
factory: &mut B::Factory,
|
|
) -> gfx::handle::Sampler<B::Resources> {
|
|
let sampler = self
|
|
.samplers
|
|
.entry(info)
|
|
.or_insert_with(|| factory.create_sampler(info));
|
|
sampler.clone()
|
|
}
|
|
}
|
|
|
|
impl From<gfx::buffer::CreationError> for GameError {
|
|
fn from(e: gfx::buffer::CreationError) -> Self {
|
|
use gfx::buffer::CreationError;
|
|
match e {
|
|
CreationError::UnsupportedBind(b) => GameError::RenderError(format!(
|
|
"Could not create buffer: Unsupported Bind ({:?})",
|
|
b
|
|
)),
|
|
CreationError::UnsupportedUsage(u) => GameError::RenderError(format!(
|
|
"Could not create buffer: Unsupported Usage ({:?})",
|
|
u
|
|
)),
|
|
CreationError::Other => {
|
|
GameError::RenderError("Could not create buffer: Unknown error".to_owned())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// **********************************************************************
|
|
// DRAWING
|
|
// **********************************************************************
|
|
|
|
/// Clear the screen to the background color.
|
|
pub fn clear(ctx: &mut Context, color: Color) {
|
|
let gfx = &mut ctx.gfx_context;
|
|
let c: [f32; 4] = color.into();
|
|
gfx.encoder.clear_raw(&gfx.data.out, c.into());
|
|
}
|
|
|
|
/// Draws the given `Drawable` object to the screen by calling its
|
|
/// [`draw()`](trait.Drawable.html#tymethod.draw) method.
|
|
pub fn draw<D, T>(ctx: &mut Context, drawable: &D, params: T) -> GameResult
|
|
where
|
|
D: Drawable,
|
|
T: Into<DrawParam>,
|
|
{
|
|
let params = params.into();
|
|
drawable.draw(ctx, params)
|
|
}
|
|
|
|
/// Tells the graphics system to actually put everything on the screen.
|
|
/// Call this at the end of your [`EventHandler`](../event/trait.EventHandler.html)'s
|
|
/// [`draw()`](../event/trait.EventHandler.html#tymethod.draw) method.
|
|
///
|
|
/// Unsets any active canvas.
|
|
pub fn present(ctx: &mut Context) -> GameResult<()> {
|
|
let gfx = &mut ctx.gfx_context;
|
|
gfx.data.out = gfx.screen_render_target.clone();
|
|
// We might want to give the user more control over when the
|
|
// encoder gets flushed eventually, if we want them to be able
|
|
// to do their own gfx drawing. HOWEVER, the whole pipeline type
|
|
// thing is a bigger hurdle, so this is fine for now.
|
|
gfx.encoder.flush(&mut *gfx.device);
|
|
gfx.window.swap_buffers()?;
|
|
gfx.device.cleanup();
|
|
Ok(())
|
|
}
|
|
|
|
/// Take a screenshot by outputting the current render surface
|
|
/// (screen or selected canvas) to an `Image`.
|
|
pub fn screenshot(ctx: &mut Context) -> GameResult<Image> {
|
|
use gfx::memory::Bind;
|
|
let debug_id = DebugId::get(ctx);
|
|
|
|
let gfx = &mut ctx.gfx_context;
|
|
let (w, h, _depth, aa) = gfx.data.out.get_dimensions();
|
|
if aa != gfx_core::texture::AaMode::Single {
|
|
// Details see https://github.com/ggez/ggez/issues/751
|
|
return Err(GameError::RenderError("Can't take screenshots of anti aliased textures.\n(since neither copying or resolving them is supported right now)".to_string()));
|
|
}
|
|
|
|
let surface_format = gfx.color_format();
|
|
let gfx::format::Format(surface_type, channel_type) = surface_format;
|
|
|
|
let texture_kind = gfx::texture::Kind::D2(w, h, aa);
|
|
let texture_info = gfx::texture::Info {
|
|
kind: texture_kind,
|
|
levels: 1,
|
|
format: surface_type,
|
|
bind: Bind::TRANSFER_SRC | Bind::TRANSFER_DST | Bind::SHADER_RESOURCE,
|
|
usage: gfx::memory::Usage::Data,
|
|
};
|
|
let target_texture = gfx
|
|
.factory
|
|
.create_texture_raw(texture_info, Some(channel_type), None)?;
|
|
let image_info = gfx::texture::ImageInfoCommon {
|
|
xoffset: 0,
|
|
yoffset: 0,
|
|
zoffset: 0,
|
|
width: w,
|
|
height: h,
|
|
depth: 0,
|
|
format: surface_format,
|
|
mipmap: 0,
|
|
};
|
|
|
|
let mut local_encoder: gfx::Encoder<gfx_device_gl::Resources, gfx_device_gl::CommandBuffer> =
|
|
gfx.factory.create_command_buffer().into();
|
|
|
|
local_encoder.copy_texture_to_texture_raw(
|
|
gfx.data.out.get_texture(),
|
|
None,
|
|
image_info,
|
|
&target_texture,
|
|
None,
|
|
image_info,
|
|
)?;
|
|
|
|
local_encoder.flush(&mut *gfx.device);
|
|
|
|
let resource_desc = gfx::texture::ResourceDesc {
|
|
channel: channel_type,
|
|
layer: None,
|
|
min: 0,
|
|
max: 0,
|
|
swizzle: gfx::format::Swizzle::new(),
|
|
};
|
|
let shader_resource = gfx
|
|
.factory
|
|
.view_texture_as_shader_resource_raw(&target_texture, resource_desc)?;
|
|
let image = Image {
|
|
texture: shader_resource,
|
|
texture_handle: target_texture,
|
|
sampler_info: gfx.default_sampler_info,
|
|
blend_mode: None,
|
|
width: w,
|
|
height: h,
|
|
debug_id,
|
|
};
|
|
|
|
Ok(image)
|
|
}
|
|
|
|
// **********************************************************************
|
|
// GRAPHICS STATE
|
|
// **********************************************************************
|
|
|
|
/// Get the default filter mode for new images.
|
|
pub fn default_filter(ctx: &Context) -> FilterMode {
|
|
let gfx = &ctx.gfx_context;
|
|
gfx.default_sampler_info.filter.into()
|
|
}
|
|
|
|
/// Returns a string that tells a little about the obtained rendering mode.
|
|
/// It is supposed to be human-readable and will change; do not try to parse
|
|
/// information out of it!
|
|
pub fn renderer_info(ctx: &Context) -> GameResult<String> {
|
|
let backend_info = ctx.gfx_context.backend_spec.info(&*ctx.gfx_context.device);
|
|
Ok(format!(
|
|
"Requested {:?} {}.{} Core profile, actually got {}.",
|
|
ctx.gfx_context.backend_spec.api,
|
|
ctx.gfx_context.backend_spec.major,
|
|
ctx.gfx_context.backend_spec.minor,
|
|
backend_info
|
|
))
|
|
}
|
|
|
|
/// Returns a rectangle defining the coordinate system of the screen.
|
|
/// It will be `Rect { x: left, y: top, w: width, h: height }`
|
|
///
|
|
/// If the Y axis increases downwards, the `height` of the `Rect`
|
|
/// will be negative.
|
|
pub fn screen_coordinates(ctx: &Context) -> Rect {
|
|
ctx.gfx_context.screen_rect
|
|
}
|
|
|
|
/// Sets the default filter mode used to scale images.
|
|
///
|
|
/// This does not apply retroactively to already created images.
|
|
pub fn set_default_filter(ctx: &mut Context, mode: FilterMode) {
|
|
let gfx = &mut ctx.gfx_context;
|
|
let new_mode = mode.into();
|
|
let sampler_info = texture::SamplerInfo::new(new_mode, texture::WrapMode::Clamp);
|
|
// We create the sampler now so we don't end up creating it at some
|
|
// random-ass time while we're trying to draw stuff.
|
|
let _sampler = gfx.samplers.get_or_insert(sampler_info, &mut *gfx.factory);
|
|
gfx.default_sampler_info = sampler_info;
|
|
}
|
|
|
|
/// Sets the bounds of the screen viewport.
|
|
///
|
|
/// The default coordinate system has (0,0) at the top-left corner
|
|
/// with X increasing to the right and Y increasing down, with the
|
|
/// viewport scaled such that one coordinate unit is one pixel on the
|
|
/// screen. This function lets you change this coordinate system to
|
|
/// be whatever you prefer.
|
|
///
|
|
/// The `Rect`'s x and y will define the top-left corner of the screen,
|
|
/// and that plus its w and h will define the bottom-right corner.
|
|
pub fn set_screen_coordinates(context: &mut Context, rect: Rect) -> GameResult {
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.set_projection_rect(rect);
|
|
gfx.calculate_transform_matrix();
|
|
gfx.update_globals()
|
|
}
|
|
|
|
/// Sets the raw projection matrix to the given homogeneous
|
|
/// transformation matrix.
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
pub fn set_projection<M>(context: &mut Context, proj: M)
|
|
where
|
|
M: Into<mint::ColumnMatrix4<f32>>,
|
|
{
|
|
let proj = Matrix4::from(proj.into());
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.set_projection(proj);
|
|
}
|
|
|
|
/// Premultiplies the given transformation matrix with the current projection matrix
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
pub fn mul_projection<M>(context: &mut Context, transform: M)
|
|
where
|
|
M: Into<mint::ColumnMatrix4<f32>>,
|
|
{
|
|
let transform = Matrix4::from(transform.into());
|
|
let gfx = &mut context.gfx_context;
|
|
let curr = gfx.projection();
|
|
gfx.set_projection(transform * curr);
|
|
}
|
|
|
|
/// Gets a copy of the context's raw projection matrix
|
|
pub fn projection(context: &Context) -> mint::ColumnMatrix4<f32> {
|
|
let gfx = &context.gfx_context;
|
|
gfx.projection().into()
|
|
}
|
|
|
|
/// Pushes a homogeneous transform matrix to the top of the transform
|
|
/// (model) matrix stack of the `Context`. If no matrix is given, then
|
|
/// pushes a copy of the current transform matrix to the top of the stack.
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
///
|
|
/// A [`DrawParam`](struct.DrawParam.html) can be converted into an appropriate
|
|
/// transform matrix by turning it into a [`DrawTransform`](struct.DrawTransform.html):
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use ggez::*;
|
|
/// # use ggez::graphics::*;
|
|
/// # fn main() {
|
|
/// # let ctx = &mut ContextBuilder::new("foo", "bar").build().unwrap().0;
|
|
/// let param = /* DrawParam building */
|
|
/// # DrawParam::new();
|
|
/// let transform = param.to_matrix();
|
|
/// graphics::push_transform(ctx, Some(transform));
|
|
/// # }
|
|
/// ```
|
|
pub fn push_transform<M>(context: &mut Context, transform: Option<M>)
|
|
where
|
|
M: Into<mint::ColumnMatrix4<f32>>,
|
|
{
|
|
let transform = transform.map(|transform| Matrix4::from(transform.into()));
|
|
let gfx = &mut context.gfx_context;
|
|
if let Some(t) = transform {
|
|
gfx.push_transform(t);
|
|
} else {
|
|
let copy = *gfx
|
|
.modelview_stack
|
|
.last()
|
|
.expect("Matrix stack empty, should never happen");
|
|
gfx.push_transform(copy);
|
|
}
|
|
}
|
|
|
|
/// Pops the transform matrix off the top of the transform
|
|
/// (model) matrix stack of the `Context`.
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
pub fn pop_transform(context: &mut Context) {
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.pop_transform();
|
|
}
|
|
|
|
/// Sets the current model transformation to the given homogeneous
|
|
/// transformation matrix.
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
///
|
|
/// A [`DrawParam`](struct.DrawParam.html) can be converted into an appropriate
|
|
/// transform matrix with `DrawParam::to_matrix()`.
|
|
/// ```rust,no_run
|
|
/// # use ggez::*;
|
|
/// # use ggez::graphics::*;
|
|
/// # fn main() {
|
|
/// # let ctx = &mut ContextBuilder::new("foo", "bar").build().unwrap().0;
|
|
/// let param = /* DrawParam building */
|
|
/// # DrawParam::new();
|
|
/// let transform = param.to_matrix();
|
|
/// graphics::set_transform(ctx, transform);
|
|
/// # }
|
|
/// ```
|
|
pub fn set_transform<M>(context: &mut Context, transform: M)
|
|
where
|
|
M: Into<mint::ColumnMatrix4<f32>>,
|
|
{
|
|
let transform = transform.into();
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.set_transform(Matrix4::from(transform));
|
|
}
|
|
|
|
/// Gets a copy of the context's current transform matrix
|
|
pub fn transform(context: &Context) -> mint::ColumnMatrix4<f32> {
|
|
let gfx = &context.gfx_context;
|
|
gfx.transform().into()
|
|
}
|
|
|
|
/// Premultiplies the given transform with the current model transform.
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
///
|
|
/// A [`DrawParam`](struct.DrawParam.html) can be converted into an appropriate
|
|
/// transform matrix by turning it into a [`DrawTransform`](struct.DrawTransform.html):
|
|
///
|
|
/// ```rust,no_run
|
|
/// # use ggez::nalgebra as na;
|
|
/// # use ggez::*;
|
|
/// # use ggez::graphics::*;
|
|
/// # fn main() {
|
|
/// # let ctx = &mut ContextBuilder::new("foo", "bar").build().unwrap().0;
|
|
/// let param = /* DrawParam building */
|
|
/// # DrawParam::new();
|
|
/// let transform = param.to_matrix();
|
|
/// graphics::mul_transform(ctx, transform);
|
|
/// # }
|
|
/// ```
|
|
pub fn mul_transform<M>(context: &mut Context, transform: M)
|
|
where
|
|
M: Into<mint::ColumnMatrix4<f32>>,
|
|
{
|
|
let transform = Matrix4::from(transform.into());
|
|
let gfx = &mut context.gfx_context;
|
|
let curr = gfx.transform();
|
|
gfx.set_transform(curr * transform);
|
|
}
|
|
|
|
/// Sets the current model transform to the origin transform (no transformation)
|
|
///
|
|
/// You must call [`apply_transformations(ctx)`](fn.apply_transformations.html)
|
|
/// after calling this to apply these changes and recalculate the
|
|
/// underlying MVP matrix.
|
|
pub fn origin(context: &mut Context) {
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.set_transform(Matrix4::identity());
|
|
}
|
|
|
|
/// Calculates the new total transformation (Model-View-Projection) matrix
|
|
/// based on the matrices at the top of the transform and view matrix stacks
|
|
/// and sends it to the graphics card.
|
|
pub fn apply_transformations(context: &mut Context) -> GameResult {
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.calculate_transform_matrix();
|
|
gfx.update_globals()
|
|
}
|
|
|
|
/// Sets the blend mode of the currently active shader program
|
|
pub fn set_blend_mode(ctx: &mut Context, mode: BlendMode) -> GameResult {
|
|
ctx.gfx_context.set_blend_mode(mode)
|
|
}
|
|
|
|
/// Sets the window mode, such as the size and other properties.
|
|
///
|
|
/// Setting the window mode may have side effects, such as clearing
|
|
/// the screen or setting the screen coordinates viewport to some
|
|
/// undefined value (for example, the window was resized). It is
|
|
/// recommended to call
|
|
/// [`set_screen_coordinates()`](fn.set_screen_coordinates.html) after
|
|
/// changing the window size to make sure everything is what you want
|
|
/// it to be.
|
|
pub fn set_mode(context: &mut Context, mode: WindowMode) -> GameResult {
|
|
let gfx = &mut context.gfx_context;
|
|
gfx.set_window_mode(mode)?;
|
|
// Save updated mode.
|
|
context.conf.window_mode = mode;
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the window to fullscreen or back.
|
|
pub fn set_fullscreen(context: &mut Context, fullscreen: conf::FullscreenType) -> GameResult {
|
|
let mut window_mode = context.conf.window_mode;
|
|
window_mode.fullscreen_type = fullscreen;
|
|
set_mode(context, window_mode)
|
|
}
|
|
|
|
/// Sets the window size/resolution to the specified width and height.
|
|
pub fn set_drawable_size(context: &mut Context, width: f32, height: f32) -> GameResult {
|
|
let mut window_mode = context.conf.window_mode;
|
|
window_mode.width = width;
|
|
window_mode.height = height;
|
|
set_mode(context, window_mode)
|
|
}
|
|
|
|
/// Sets whether or not the window is resizable.
|
|
pub fn set_resizable(context: &mut Context, resizable: bool) -> GameResult {
|
|
let mut window_mode = context.conf.window_mode;
|
|
window_mode.resizable = resizable;
|
|
set_mode(context, window_mode)
|
|
}
|
|
|
|
/// Sets the window icon.
|
|
pub fn set_window_icon<P: AsRef<Path>>(context: &mut Context, path: Option<P>) -> GameResult<()> {
|
|
let icon = match path {
|
|
Some(p) => {
|
|
let p: &Path = p.as_ref();
|
|
Some(context::load_icon(p, &mut context.filesystem)?)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
context.gfx_context.window.window().set_window_icon(icon);
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the window title.
|
|
pub fn set_window_title(context: &Context, title: &str) {
|
|
context.gfx_context.window.window().set_title(title);
|
|
}
|
|
|
|
/// Returns a reference to the Glutin window.
|
|
/// Ideally you should not need to use this because ggez
|
|
/// would provide all the functions you need without having
|
|
/// to dip into Glutin itself. But life isn't always ideal.
|
|
pub fn window(context: &Context) -> &glutin::WindowedContext<PossiblyCurrent> {
|
|
let gfx = &context.gfx_context;
|
|
&gfx.window
|
|
}
|
|
|
|
/// Returns the size of the window in pixels as (width, height),
|
|
/// including borders, titlebar, etc.
|
|
/// Returns zeros if the window doesn't exist.
|
|
pub fn size(context: &Context) -> (f32, f32) {
|
|
let gfx = &context.gfx_context;
|
|
let size = gfx.window.window().outer_size();
|
|
(size.width as f32, size.height as f32)
|
|
}
|
|
|
|
/// Returns the size of the window's underlying drawable in pixels as (width, height).
|
|
/// Returns zeros if window doesn't exist.
|
|
pub fn drawable_size(context: &Context) -> (f32, f32) {
|
|
let gfx = &context.gfx_context;
|
|
let size = gfx.window.window().inner_size();
|
|
(size.width as f32, size.height as f32)
|
|
}
|
|
|
|
/// Returns raw `gfx-rs` state objects, if you want to use `gfx-rs` to write
|
|
/// your own graphics pipeline then this gets you the interfaces you need
|
|
/// to do so.
|
|
///
|
|
/// Returns all the relevant objects at once;
|
|
/// getting them one by one is awkward 'cause it tends to create double-borrows
|
|
/// on the Context object.
|
|
pub fn gfx_objects(
|
|
context: &mut Context,
|
|
) -> (
|
|
&mut <GlBackendSpec as BackendSpec>::Factory,
|
|
&mut <GlBackendSpec as BackendSpec>::Device,
|
|
&mut gfx::Encoder<
|
|
<GlBackendSpec as BackendSpec>::Resources,
|
|
<GlBackendSpec as BackendSpec>::CommandBuffer,
|
|
>,
|
|
gfx::handle::RawDepthStencilView<<GlBackendSpec as BackendSpec>::Resources>,
|
|
gfx::handle::RawRenderTargetView<<GlBackendSpec as BackendSpec>::Resources>,
|
|
) {
|
|
let gfx = &mut context.gfx_context;
|
|
let f = &mut gfx.factory;
|
|
let d = gfx.device.as_mut();
|
|
let e = &mut gfx.encoder;
|
|
let dv = gfx.depth_view.clone();
|
|
let cv = gfx.data.out.clone();
|
|
(f, d, e, dv, cv)
|
|
}
|
|
|
|
/// All types that can be drawn on the screen implement the `Drawable` trait.
|
|
pub trait Drawable {
|
|
/// Draws the drawable onto the rendering target.
|
|
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult;
|
|
|
|
/// Returns a bounding box in the form of a `Rect`.
|
|
///
|
|
/// It returns `Option` because some `Drawable`s may have no bounding box
|
|
/// (an empty `SpriteBatch` for example).
|
|
fn dimensions(&self, ctx: &mut Context) -> Option<Rect>;
|
|
|
|
/// Sets the blend mode to be used when drawing this drawable.
|
|
/// This overrides the general [`graphics::set_blend_mode()`](fn.set_blend_mode.html).
|
|
/// If `None` is set, defers to the blend mode set by
|
|
/// `graphics::set_blend_mode()`.
|
|
fn set_blend_mode(&mut self, mode: Option<BlendMode>);
|
|
|
|
/// Gets the blend mode to be used when drawing this drawable.
|
|
fn blend_mode(&self) -> Option<BlendMode>;
|
|
}
|
|
|
|
/// Applies `DrawParam` to `Rect`.
|
|
pub fn transform_rect(rect: Rect, param: DrawParam) -> Rect {
|
|
let w = param.src.w * param.scale.x * rect.w;
|
|
let h = param.src.h * param.scale.y * rect.h;
|
|
let offset_x = w * param.offset.x;
|
|
let offset_y = h * param.offset.y;
|
|
let dest_x = param.dest.x - offset_x;
|
|
let dest_y = param.dest.y - offset_y;
|
|
let mut r = Rect {
|
|
w,
|
|
h,
|
|
x: dest_x + rect.x * param.scale.x,
|
|
y: dest_y + rect.y * param.scale.y,
|
|
};
|
|
r.rotate(param.rotation);
|
|
r
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::f32::consts::PI;
|
|
|
|
use approx::assert_relative_eq;
|
|
|
|
use crate::graphics::{DrawParam, Rect, transform_rect};
|
|
|
|
#[test]
|
|
fn headless_test_transform_rect() {
|
|
{
|
|
let r = Rect {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
w: 1.0,
|
|
h: 1.0,
|
|
};
|
|
let param = DrawParam::new();
|
|
let real = transform_rect(r, param);
|
|
let expected = r;
|
|
assert_relative_eq!(real, expected);
|
|
}
|
|
{
|
|
let r = Rect {
|
|
x: -1.0,
|
|
y: -1.0,
|
|
w: 2.0,
|
|
h: 1.0,
|
|
};
|
|
let param = DrawParam::new().scale([0.5, 0.5]);
|
|
let real = transform_rect(r, param);
|
|
let expected = Rect {
|
|
x: -0.5,
|
|
y: -0.5,
|
|
w: 1.0,
|
|
h: 0.5,
|
|
};
|
|
assert_relative_eq!(real, expected);
|
|
}
|
|
{
|
|
let r = Rect {
|
|
x: -1.0,
|
|
y: -1.0,
|
|
w: 1.0,
|
|
h: 1.0,
|
|
};
|
|
let param = DrawParam::new().offset([0.5, 0.5]);
|
|
let real = transform_rect(r, param);
|
|
let expected = Rect {
|
|
x: -1.5,
|
|
y: -1.5,
|
|
w: 1.0,
|
|
h: 1.0,
|
|
};
|
|
assert_relative_eq!(real, expected);
|
|
}
|
|
{
|
|
let r = Rect {
|
|
x: 1.0,
|
|
y: 0.0,
|
|
w: 2.0,
|
|
h: 1.0,
|
|
};
|
|
let param = DrawParam::new().rotation(PI * 0.5);
|
|
let real = transform_rect(r, param);
|
|
let expected = Rect {
|
|
x: -1.0,
|
|
y: 1.0,
|
|
w: 1.0,
|
|
h: 2.0,
|
|
};
|
|
assert_relative_eq!(real, expected);
|
|
}
|
|
{
|
|
let r = Rect {
|
|
x: -1.0,
|
|
y: -1.0,
|
|
w: 2.0,
|
|
h: 1.0,
|
|
};
|
|
let param = DrawParam::new()
|
|
.scale([0.5, 0.5])
|
|
.offset([0.0, 1.0])
|
|
.rotation(PI * 0.5);
|
|
let real = transform_rect(r, param);
|
|
let expected = Rect {
|
|
x: 0.5,
|
|
y: -0.5,
|
|
w: 0.5,
|
|
h: 1.0,
|
|
};
|
|
assert_relative_eq!(real, expected);
|
|
}
|
|
}
|
|
}
|