Skip to content

Commit

Permalink
Define Pixmap and PixmapAtlas to abstract image loading/mgmt (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
nogginly committed May 23, 2022
1 parent 9e3612d commit e321954
Show file tree
Hide file tree
Showing 20 changed files with 603 additions and 153 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -2,3 +2,5 @@ build/
test/screenshot*
*.gem
Gemfile.lock
.DS_Store
tmp*
6 changes: 3 additions & 3 deletions ext/ruby2d/gl.c
Expand Up @@ -299,7 +299,7 @@ int R2D_GL_Init(R2D_Window *window) {
/*
* Creates a texture for rendering
*/
void R2D_GL_CreateTexture(GLuint *id, GLint format,
void R2D_GL_CreateTexture(GLuint *id, GLint internalFormat, GLint format, GLenum type,
int w, int h,
const GLvoid *data, GLint filter) {

Expand All @@ -311,8 +311,8 @@ void R2D_GL_CreateTexture(GLuint *id, GLint format,

// Specifies the 2D texture image
glTexImage2D(
GL_TEXTURE_2D, 0, format, w, h,
0, format, GL_UNSIGNED_BYTE, data
GL_TEXTURE_2D, 0, internalFormat, w, h,
0, format, type, data
);

// Set the filtering mode
Expand Down
14 changes: 7 additions & 7 deletions ext/ruby2d/image.c
Expand Up @@ -14,15 +14,15 @@ SDL_Surface *R2D_CreateImageSurface(const char *path) {

// Load image from file as SDL_Surface
SDL_Surface *surface = IMG_Load(path);
if (surface != NULL) {
int bits_per_color = surface->format->Amask == 0 ?
surface->format->BitsPerPixel / 3 :
surface->format->BitsPerPixel / 4;

int bits_per_color = surface->format->Amask == 0 ?
surface->format->BitsPerPixel / 3 :
surface->format->BitsPerPixel / 4;

if (bits_per_color < 8) {
R2D_Log(R2D_WARN, "`%s` has less than 8 bits per color and will likely not render correctly", path, bits_per_color);
if (bits_per_color < 8) {
R2D_Log(R2D_WARN, "`%s` has less than 8 bits per color and will likely not render correctly", path, bits_per_color);
}
}

return surface;
}

Expand Down
173 changes: 151 additions & 22 deletions ext/ruby2d/ruby2d.c
Expand Up @@ -133,6 +133,10 @@ static void free_surface(mrb_state *mrb, void *p_);
static const struct mrb_data_type surface_data_type = {
"surface", free_surface
};
static void free_sdl_texture(mrb_state *mrb, void *p_);
static const struct mrb_data_type sdl_texture_data_type = {
"sdl_texture", free_sdl_texture
};
static void free_renderer(mrb_state *mrb, void *p_);
static const struct mrb_data_type renderer_data_type = {
"renderer", free_renderer
Expand All @@ -142,6 +146,7 @@ static void free_renderer(mrb_state *mrb, void *p_);
static void free_music(R2D_Music *mus);
static void free_font(TTF_Font *font);
static void free_surface(SDL_Surface *surface);
static void free_sdl_texture(SDL_Texture *surface);
static void free_renderer(SDL_Renderer *renderer);
#endif

Expand Down Expand Up @@ -377,31 +382,46 @@ static R_VAL ruby2d_circle_ext_draw(R_VAL self, R_VAL a) {


/*
* Ruby2D::Image#ext_load_image
* Create an SDL surface from an image path, return the surface, width, and height
* Ruby2D::Pixmap#ext_load_pixmap
* Create an SDL surface from an image path, and ...
* TODO: store the surface, width, and height in the object
*/
#if MRUBY
static R_VAL ruby2d_image_ext_load_image(mrb_state* mrb, R_VAL self) {
static R_VAL ruby2d_pixmap_ext_load_pixmap(mrb_state* mrb, R_VAL self) {
mrb_value path;
mrb_get_args(mrb, "o", &path);
#else
static R_VAL ruby2d_image_ext_load_image(R_VAL self, R_VAL path) {
static R_VAL ruby2d_pixmap_ext_load_pixmap(R_VAL self, R_VAL path) {
#endif
R2D_Init();

R_VAL result = r_ary_new();

SDL_Surface *surface = R2D_CreateImageSurface(RSTRING_PTR(path));
R2D_ImageConvertToRGB(surface);

r_ary_push(result, r_data_wrap_struct(surface, surface));
r_ary_push(result, INT2NUM(surface->w));
r_ary_push(result, INT2NUM(surface->h));
// initialize for later use
r_iv_set(self, "@ext_sdl_texture", R_NIL);

if (surface != NULL) {
#if GLES
// OpenGL ES doesn't support BGR color order, so applying the
// conversion to the pixels. This will however render incorrect colours
// for BGR images when using Canvas.
// TODO: A better solution one day?
R2D_ImageConvertToRGB(surface);
#endif

return result;
r_iv_set(self, "@ext_pixel_data", r_data_wrap_struct(surface, surface));
r_iv_set(self, "@width", INT2NUM(surface->w));
r_iv_set(self, "@height", INT2NUM(surface->h));
}
else {
// TODO: consider raising an exception?
// for docs: https://silverhammermba.github.io/emberb/c/#raise
r_iv_set(self, "@ext_pixel_data", R_NIL);
r_iv_set(self, "@width", INT2NUM(0));
r_iv_set(self, "@height", INT2NUM(0));
}
return R_NIL;
}


/*
* Ruby2D::Text#ext_load_text
*/
Expand Down Expand Up @@ -436,6 +456,69 @@ static R_VAL ruby2d_text_ext_load_text(R_VAL self, R_VAL font, R_VAL message) {
return result;
}

/*
* Ruby2D::Canvas#ext_draw_pixmap
*/
#if MRUBY
static R_VAL ruby2d_canvas_ext_draw_pixmap(mrb_state* mrb, R_VAL self) {
mrb_value pixmap, src_rect, x, y, w, h;
mrb_get_args(mrb, "oooooo", &pixmap, &src_rect, &x, &y, &w, &h);
#else
static R_VAL ruby2d_canvas_ext_draw_pixmap(R_VAL self, R_VAL pixmap, R_VAL src_rect, R_VAL x, R_VAL y, R_VAL w, R_VAL h) {
#endif

if (r_test(pixmap)) {
// Retrieve pixmap's external pixel data (SDL_Surface)
SDL_Surface *pix_surface;
r_data_get_struct(pixmap, "@ext_pixel_data", &surface_data_type, SDL_Surface, pix_surface);

SDL_Texture *pix_sdl_tex = NULL;
R_VAL pix_ext_sdl_tex = r_iv_get(pixmap, "@ext_sdl_texture");
if (r_test(pix_ext_sdl_tex)) {
r_data_get_struct(pixmap, "@ext_sdl_texture", &sdl_texture_data_type, SDL_Texture, pix_sdl_tex);
}

SDL_Renderer *render;
r_data_get_struct(self, "@ext_renderer", &renderer_data_type, SDL_Renderer, render);

if (pix_sdl_tex == NULL) {
// create and cache an SDL_Texture for this Pixmap
pix_sdl_tex = SDL_CreateTextureFromSurface(render, pix_surface);
if (pix_sdl_tex != NULL) {
r_iv_set(pixmap, "@ext_sdl_texture", r_data_wrap_struct(sdl_texture, pix_sdl_tex));
}
else printf("*** Unable to create SDL_Texture: %s\n", SDL_GetError());
}

// Draw if we have an SDL_Texture
if (pix_sdl_tex != NULL) {
SDL_bool src_set = SDL_FALSE;
SDL_Rect src;
if (r_test(src_rect)) {
src_set = SDL_TRUE;
// portion of pixmap
src = (SDL_Rect) {
.x = NUM2INT(r_ary_entry(src_rect, 0)),
.y = NUM2INT(r_ary_entry(src_rect, 1)),
.w = NUM2INT(r_ary_entry(src_rect, 2)),
.h = NUM2INT(r_ary_entry(src_rect, 3))
};
}
else {
// whole pixmap
src = (SDL_Rect){ .x = 0, .y = 0, .w = pix_surface->w, .h = pix_surface->h };
}
// use incoming size or source size or fallback to pixmap size
int pix_w = r_test(w) ? NUM2INT(w) : (src_set ? src.w : NUM2INT(r_iv_get(pixmap, "@width")));
int pix_h = r_test(h) ? NUM2INT(h) : (src_set ? src.h : NUM2INT(r_iv_get(pixmap, "@height")));

SDL_Rect dst = { .x = NUM2INT(x), .y = NUM2INT(y), .w = pix_w, .h = pix_h };
SDL_RenderCopy (render, pix_sdl_tex, &src, &dst);
}
}
return R_NIL;
}


/*
* Ruby2D::Texture#ext_create
Expand All @@ -457,19 +540,47 @@ static R_VAL ruby2d_texture_ext_create(R_VAL self, R_VAL rubySurface, R_VAL widt
#endif

// Detect image mode
GLint format = GL_RGB;
if (surface->format->BytesPerPixel == 4) {
format = GL_RGBA;
}
GLint gl_internal_format, gl_format;
GLenum gl_type;

R2D_GL_CreateTexture(&texture_id, format,
switch (surface->format->BytesPerPixel) {
case 4:
#if GLES
gl_internal_format = gl_format = GL_RGBA;
#else
gl_internal_format = GL_RGBA;
gl_format = (surface->format->Rmask == 0xff0000) ? GL_BGRA : GL_RGBA;
#endif
gl_type = GL_UNSIGNED_BYTE;
break;
case 3:
#if GLES
gl_internal_format = gl_format = GL_RGB;
#else
gl_internal_format = GL_RGB;
gl_format = (surface->format->Rmask == 0xff0000) ? GL_BGR : GL_RGB;
#endif
gl_type = GL_UNSIGNED_BYTE;
break;
case 2:
gl_internal_format = gl_format = GL_RGB;
gl_type = GL_UNSIGNED_SHORT_5_6_5;
break;
case 1:
default:
// this would be ideal for font glyphs which use luminance + alpha and colour
// is set when drawing
gl_internal_format = gl_format = GL_LUMINANCE_ALPHA;
gl_type = GL_UNSIGNED_BYTE;
break;
}
R2D_GL_CreateTexture(&texture_id, gl_internal_format, gl_format, gl_type,
NUM2INT(width), NUM2INT(height),
surface->pixels, GL_NEAREST);

return INT2NUM(texture_id);
}


/*
* Ruby2D::Texture#ext_delete
*/
Expand Down Expand Up @@ -1324,6 +1435,18 @@ static void free_surface(SDL_Surface *surface) {
SDL_FreeSurface(surface);
}

/*
* Free SDL texture structure used within the Ruby 2D `Pixmap` class
*/
#if MRUBY
static void free_sdl_texture(mrb_state *mrb, void *p_) {
SDL_Texture *sdl_texure = (SDL_Texture *)p_;
#else
static void free_sdl_texture(SDL_Texture *sdl_texure) {
#endif
SDL_DestroyTexture(sdl_texure);
}

/*
* Free renderer structure used within the Ruby 2D `Canvas` class
*/
Expand Down Expand Up @@ -1774,12 +1897,15 @@ void Init_ruby2d() {
// Ruby2D::Circle#self.ext_draw
r_define_class_method(ruby2d_circle_class, "ext_draw", ruby2d_circle_ext_draw, r_args_req(1));

// Ruby2D::Pixmap
R_CLASS ruby2d_pixmap_class = r_define_class(ruby2d_module, "Pixmap");

// Ruby2D::Pixmap#ext_load_pixmap
r_define_method(ruby2d_pixmap_class, "ext_load_pixmap", ruby2d_pixmap_ext_load_pixmap, r_args_req(1));

// Ruby2D::Image
R_CLASS ruby2d_image_class = r_define_class(ruby2d_module, "Image");

// Ruby2D::Image#ext_load_image
r_define_class_method(ruby2d_image_class, "ext_load_image", ruby2d_image_ext_load_image, r_args_req(1));

// Ruby2D::Text
R_CLASS ruby2d_text_class = r_define_class(ruby2d_module, "Text");

Expand Down Expand Up @@ -1891,6 +2017,9 @@ void Init_ruby2d() {
// Ruby2D::Canvas#ext_draw_ellipse
r_define_method(ruby2d_canvas_class, "ext_draw_ellipse", ruby2d_canvas_ext_draw_ellipse, r_args_req(1));

// Ruby2D::Canvas#ext_draw_pixmap
r_define_method(ruby2d_canvas_class, "ext_draw_pixmap", ruby2d_canvas_ext_draw_pixmap, r_args_req(6));

// Ruby2D::Window
R_CLASS ruby2d_window_class = r_define_class(ruby2d_module, "Window");

Expand Down
2 changes: 1 addition & 1 deletion ext/ruby2d/ruby2d.h
Expand Up @@ -696,7 +696,7 @@ int R2D_GL_CheckLinked(GLuint program, const char *name);
void R2D_GL_GetViewportScale(R2D_Window *window, int *w, int *h, double *scale);
void R2D_GL_SetViewport(R2D_Window *window);
void R2D_GL_CreateTexture(
GLuint *id, GLint format,
GLuint *id, GLint internalFormat, GLint format, GLenum type,
int w, int h,
const GLvoid *data, GLint filter);
void R2D_GL_DrawTriangle(
Expand Down
2 changes: 2 additions & 0 deletions lib/ruby2d.rb
Expand Up @@ -15,6 +15,8 @@
require 'ruby2d/square'
require 'ruby2d/triangle'
require 'ruby2d/pixel'
require 'ruby2d/pixmap'
require 'ruby2d/pixmap_atlas'
require 'ruby2d/image'
require 'ruby2d/sprite'
require 'ruby2d/tileset'
Expand Down
14 changes: 14 additions & 0 deletions lib/ruby2d/canvas.rb
Expand Up @@ -239,6 +239,20 @@ def fill_polygon(coordinates:, color: nil, colour: nil)
update_texture if @update
end

# Draw the pixmap at the specified location and size
#
# @note This API will evolve to be able to draw a portion of the pixmap; coming soon.
# @param [Pixmap] pixmap
# @param [Numeric] x
# @param [Numeric] y
# @param [Numeric] width Optional, specify to scale the size
# @param [Numeric] height Optional, specify to scale the size
# @param [Hash] crop Optional, specify a hash with `x:, y:, width:, height:` to crop from within the pixmap to draw.
def draw_pixmap(pixmap, x:, y:, width: nil, height: nil, crop: nil)
src_rect = crop ? [crop[:x], crop[:y], crop[:width], crop[:height]] : nil
ext_draw_pixmap pixmap, src_rect, x, y, width, height
end

def update
update_texture
end
Expand Down
2 changes: 2 additions & 0 deletions lib/ruby2d/cli/build.rb
Expand Up @@ -22,6 +22,8 @@
'square',
'triangle',
'pixel',
'pixmap',
'pixmap_atlas',
'image',
'sprite',
'tileset',
Expand Down
19 changes: 12 additions & 7 deletions lib/ruby2d/image.rb
Expand Up @@ -7,18 +7,23 @@ class Image
attr_reader :path
attr_accessor :x, :y, :width, :height, :rotate, :data

def self.load_image(path)
unless File.exist? path
raise Error, "Cannot find image file `#{path}`"
end

ext_load_image(path)
# Load an image +path+ and return a Texture, using a pixmap atlas if provided
# @param [PixmapAtlas] Optional pixmap atlas to use to manage the image file
# @return [Texture] loaded
def self.load_image_as_texture(path, atlas:)
pixmap = if atlas
atlas.load_and_keep_image(path, as: path)
else
Pixmap.new path
end
pixmap.texture
end

def initialize(path, opts = {})
@path = path

@texture = Texture.new(*Image.load_image(@path))
# Consider input pixmap atlas if supplied to load image file
@texture = Image.load_image_as_texture path, atlas: opts[:atlas]
@width = opts[:width] || @texture.width
@height = opts[:height] || @texture.height

Expand Down

0 comments on commit e321954

Please sign in to comment.