v2 / vlib / gg / image.c.v
450 lines · 404 sloc · 13.32 KB · cbd757006e786b9c31dc42535a9147a242da26bb
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module gg
4
5import os
6import stbi
7import sokol.gfx
8import sokol.sgl
9
10// Image holds the fields and data needed to
11// represent a bitmap/pixel based image in memory.
12@[heap; markused]
13pub struct Image {
14pub mut:
15 id int
16 width int
17 height int
18 nr_channels int
19 ok bool
20 data voidptr
21 ext string
22 simg_ok bool
23 simg gfx.Image
24 ssmp gfx.Sampler
25 path string
26 texture_filter TextureFilter = .linear
27}
28
29// destroy GPU resources associated with the image
30fn (image &Image) destroy() {
31 if image.ok {
32 if image.simg.id > 0 {
33 gfx.destroy_image(image.simg)
34 }
35 if image.ssmp.id > 0 {
36 gfx.destroy_sampler(image.ssmp)
37 }
38 }
39}
40
41// create_image creates an `Image` from `file`.
42pub fn (mut ctx Context) create_image(file string) !Image {
43 return ctx.create_image_with_filter(file, ctx.config.texture_filter)
44}
45
46// create_image_with_filter creates an `Image` from `file` with the requested texture filter.
47pub fn (mut ctx Context) create_image_with_filter(file string, texture_filter TextureFilter) !Image {
48 if !os.exists(file) {
49 $if android {
50 image_data := os.read_apk_asset(file)!
51 mut image := ctx.create_image_from_byte_array_with_filter(image_data, texture_filter)!
52
53 image.path = file
54
55 return image
56 } $else {
57 return error('image file "${file}" not found')
58 }
59 }
60
61 $if macos {
62 if ctx.native_rendering {
63 // return C.darwin_create_image(file)
64 mut img := C.darwin_create_image(file)
65 img.texture_filter = texture_filter
66
67 // println('created macos image: ${img.path} w=${img.width}')
68 // C.printf('p = %p\n', img.data)
69 img.id = ctx.image_cache.len
70 unsafe {
71 ctx.image_cache << img
72 }
73 return img
74 }
75 }
76
77 if !gfx.is_valid() {
78 // Sokol is not initialized yet, add stbi object to a queue/cache
79 // ctx.image_queue << file
80 mut img := load_image_with_filter(file, texture_filter)!
81 img.ok = false
82 img.id = ctx.image_cache.len
83 unsafe {
84 ctx.image_cache << img
85 }
86 return img
87 }
88 mut img := create_image(file, texture_filter)!
89 img.id = ctx.image_cache.len
90 unsafe {
91 ctx.image_cache << img
92 }
93 return img
94}
95
96// init_sokol_image initializes this `Image` for use with the
97// sokol graphical backend system.
98pub fn (mut img Image) init_sokol_image() &Image {
99 // println('\n init sokol image ${img.path} ok=${img.simg_ok}')
100 mut img_desc := gfx.ImageDesc{
101 width: img.width
102 height: img.height
103 num_mipmaps: 0
104 // wrap_u: .clamp_to_edge // XTODO SAMPLER
105 // wrap_v: .clamp_to_edge
106 label: &char(img.path.str)
107 d3d11_texture: 0
108 }
109
110 // NOTE the following code, sometimes, result in hard-to-detect visual errors/bugs:
111 // img_size := usize(img.nr_channels * img.width * img.height)
112 // As an example see https://github.com/vlang/vab/issues/239
113 // The image will come out blank for some reason and no SOKOL_ASSERT
114 // nor any CI check will/can currently catch this.
115 // Since all of gg currently runs with more or less *defaults* from sokol_gfx/sokol_gl
116 // we should currently just use the sum of each of the RGB and A channels (= 4) here instead.
117 // Optimized PNG images that have no alpha channel is often optimized to only have
118 // 3 (or less) channels which stbi will correctly detect and set as `img.nr_channels`
119 // but the current sokol_gl context setup expects 4. It *should* be the same with
120 // all other stbi supported formats.
121 img_size := usize(4 * img.width * img.height)
122 img_desc.data.subimage[0][0] = gfx.Range{
123 ptr: img.data
124 size: img_size
125 }
126 img.simg = gfx.make_image(&img_desc)
127 gfx_filter := img.texture_filter.gfx_filter()
128
129 mut smp_desc := gfx.SamplerDesc{
130 min_filter: gfx_filter
131 mag_filter: gfx_filter
132 wrap_u: .clamp_to_edge
133 wrap_v: .clamp_to_edge
134 }
135
136 img.ssmp = gfx.make_sampler(&smp_desc)
137
138 img.simg_ok = true
139 img.ok = true
140 return img
141}
142
143// draw_image draws the provided image onto the screen.
144pub fn (ctx &Context) draw_image(x f32, y f32, width f32, height f32, img_ &Image) {
145 ctx.draw_image_with_config(
146 img: img_
147 img_rect: Rect{x, y, width, height}
148 part_rect: Rect{0, 0, img_.width, img_.height}
149 )
150}
151
152// new_streaming_image returns a cached `image_idx` of a special image, that
153// can be updated *each frame* by calling: gg.update_pixel_data(image_idx, buf)
154// ... where buf is a pointer to the actual pixel data for the image.
155// Note: you still need to call app.gg.draw_image after that, to actually draw it.
156// Note: Sokol needs to be setup, *before* calling this function. In practice,
157// this often means, that you have to call it once in the `init_fn` callback of
158// gg.new_context, or gg.start, and then store the result in your app instance.
159pub fn (mut ctx Context) new_streaming_image(w int, h int, channels int, sicfg StreamingImageConfig) int {
160 mut img := Image{}
161 img.width = w
162 img.height = h
163 img.nr_channels = channels // 4 bytes per pixel for .rgba8, see pixel_format
164 mut img_desc := gfx.ImageDesc{
165 width: img.width
166 height: img.height
167 pixel_format: sicfg.pixel_format
168 num_slices: 1
169 num_mipmaps: 1
170 usage: .stream
171 label: &char(img.path.str)
172 }
173 // Sokol requires that streamed images have NO .ptr/.size initially:
174 img_desc.data.subimage[0][0] = gfx.Range{
175 ptr: 0
176 size: usize(0)
177 }
178 img.simg = gfx.make_image(&img_desc)
179
180 mut smp_desc := gfx.SamplerDesc{
181 wrap_u: sicfg.wrap_u // SAMPLER
182 wrap_v: sicfg.wrap_v
183 min_filter: sicfg.min_filter
184 mag_filter: sicfg.mag_filter
185 }
186
187 img.ssmp = gfx.make_sampler(&smp_desc)
188 img.simg_ok = true
189 img.ok = true
190 img_idx := ctx.cache_image(img)
191 return img_idx
192}
193
194// update_pixel_data is a helper for working with image streams (i.e. images,
195// that are updated dynamically by the CPU on each frame)
196pub fn (mut ctx Context) update_pixel_data(cached_image_idx int, buf &u8) {
197 mut image := ctx.get_cached_image_by_idx(cached_image_idx)
198 image.update_pixel_data(buf)
199}
200
201// update_pixel_data updates the sokol specific pixel data associated
202// with this `Image`.
203pub fn (mut img Image) update_pixel_data(buf &u8) {
204 mut data := gfx.ImageData{}
205 data.subimage[0][0].ptr = buf
206 data.subimage[0][0].size = usize(img.width * img.height * img.nr_channels)
207 gfx.update_image(img.simg, &data)
208}
209
210// create_image_with_size creates an `Image` from `file` in the given
211// `width` x `height` dimension.
212//
213// TODO: copypasta
214@[deprecated]
215pub fn (mut ctx Context) create_image_with_size(file string, width int, height int) Image {
216 if !gfx.is_valid() {
217 // Sokol is not initialized yet, add stbi object to a queue/cache
218 // ctx.image_queue << file
219 mut img := load_image_with_filter(file, ctx.config.texture_filter) or { return Image{} }
220 img.width = width
221 img.height = height
222 img.ok = false
223 img.id = ctx.image_cache.len
224 ctx.image_cache << img
225 return img
226 }
227 mut img := create_image(file, ctx.config.texture_filter) or { return Image{} }
228 img.id = ctx.image_cache.len
229 ctx.image_cache << img
230 return img
231}
232
233// create_image creates an `Image` from `file`.
234//
235// TODO: remove this
236fn create_image(file string, texture_filter TextureFilter) !Image {
237 mut img := load_image_with_filter(file, texture_filter)!
238 img.init_sokol_image()
239 return img
240}
241
242fn load_image_with_filter(file string, texture_filter TextureFilter) !Image {
243 mut img := load_image(file)!
244 img.texture_filter = texture_filter
245 return img
246}
247
248fn load_image(file string) !Image {
249 if !os.exists(file) {
250 return error('image file "${file}" not found')
251 }
252 stb_img := stbi.load(file)!
253 return Image{
254 width: stb_img.width
255 height: stb_img.height
256 nr_channels: stb_img.nr_channels
257 ok: stb_img.ok
258 data: stb_img.data
259 ext: stb_img.ext
260 path: file
261 }
262}
263
264// create_image_from_memory creates an `Image` from the
265// memory buffer `buf` of size `bufsize`.
266//
267// See also: create_image_from_byte_array
268pub fn (mut ctx Context) create_image_from_memory(buf &u8, bufsize int) !Image {
269 return ctx.create_image_from_memory_with_filter(buf, bufsize, ctx.config.texture_filter)
270}
271
272// create_image_from_memory_with_filter creates an `Image` from `buf` with the requested texture filter.
273pub fn (mut ctx Context) create_image_from_memory_with_filter(buf &u8, bufsize int, texture_filter TextureFilter) !Image {
274 stb_img := stbi.load_from_memory(buf, bufsize)!
275 mut img := Image{
276 width: stb_img.width
277 height: stb_img.height
278 nr_channels: stb_img.nr_channels
279 ok: stb_img.ok
280 data: stb_img.data
281 ext: stb_img.ext
282 id: ctx.image_cache.len
283 texture_filter: texture_filter
284 }
285 if gfx.is_valid() && !ctx.native_rendering {
286 img.init_sokol_image()
287 }
288 ctx.image_cache << img
289 return img
290}
291
292// create_image_from_byte_array creates an `Image` from the
293// byte array `b`.
294//
295// See also: create_image_from_memory
296pub fn (mut ctx Context) create_image_from_byte_array(b []u8) !Image {
297 return ctx.create_image_from_byte_array_with_filter(b, ctx.config.texture_filter)
298}
299
300// create_image_from_byte_array_with_filter creates an `Image` from `b` with the requested texture filter.
301pub fn (mut ctx Context) create_image_from_byte_array_with_filter(b []u8, texture_filter TextureFilter) !Image {
302 return ctx.create_image_from_memory_with_filter(b.data, b.len, texture_filter)
303}
304
305pub struct StreamingImageConfig {
306pub:
307 pixel_format gfx.PixelFormat = .rgba8
308 wrap_u gfx.Wrap = .clamp_to_edge
309 wrap_v gfx.Wrap = .clamp_to_edge
310 min_filter gfx.Filter = .linear
311 mag_filter gfx.Filter = .linear
312 num_mipmaps int = 1
313 num_slices int = 1
314}
315
316fn (filter TextureFilter) gfx_filter() gfx.Filter {
317 return match filter {
318 .linear { .linear }
319 .nearest { .nearest }
320 }
321}
322
323// draw_image_with_config takes in a config that details how the
324// provided image should be drawn onto the screen
325pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
326 $if macos {
327 if ctx.native_rendering {
328 unsafe {
329 mut img := config.img
330 if config.img == nil {
331 // Get image by id
332 if config.img_id > 0 {
333 img = &ctx.image_cache[config.img_id]
334 } else {
335 $if !noggverbose ? {
336 eprintln('gg: failed to get image to draw natively')
337 }
338 return
339 }
340 }
341 if img.id >= ctx.image_cache.len {
342 eprintln('gg: draw_image() bad img id ${img.id} (img cache len = ${ctx.image_cache.len})')
343 return
344 }
345 if img.width == 0 {
346 println('gg: draw_image() width=0')
347 return
348 }
349 if !os.exists(img.path) {
350 println('not exist path')
351 return
352 }
353 x := config.img_rect.x
354 y := config.img_rect.y
355 width := if config.img_rect.width == 0 {
356 // Calculate the width by dividing it by the height ratio.
357 // e.g. the original image is 100x100, we're drawing 0x20. Find the ratio (5)
358 // by dividing the height 100 by 20, and then divide the width by 5.
359 f32(img.width / (img.height / config.img_rect.height))
360 } else {
361 config.img_rect.width
362 }
363 height := if config.img_rect.height == 0 {
364 // Same as above.
365 f32(img.height / (img.width / config.img_rect.width))
366 } else {
367 config.img_rect.height
368 }
369 C.darwin_draw_image(x, ctx.height - (y + config.img_rect.height), width, height,
370 img)
371 return
372 }
373 }
374 }
375
376 id := if !isnil(config.img) { config.img.id } else { config.img_id }
377 if id >= ctx.image_cache.len {
378 eprintln('gg: draw_image() bad img id ${id} (img cache len = ${ctx.image_cache.len})')
379 return
380 }
381
382 img := &ctx.image_cache[id]
383 if !img.simg_ok {
384 return
385 }
386
387 mut img_rect := config.img_rect
388 if img_rect.width == 0 && img_rect.height == 0 {
389 img_rect = Rect{img_rect.x, img_rect.y, img.width, img.height}
390 }
391
392 mut part_rect := config.part_rect
393 if part_rect.width == 0 && part_rect.height == 0 {
394 part_rect = Rect{part_rect.x, part_rect.y, img.width, img.height}
395 }
396
397 u0 := part_rect.x / img.width
398 v0 := part_rect.y / img.height
399 u1 := (part_rect.x + part_rect.width) / img.width
400 v1 := (part_rect.y + part_rect.height) / img.height
401 x0 := img_rect.x * ctx.scale
402 y0 := img_rect.y * ctx.scale
403 x1 := (img_rect.x + img_rect.width) * ctx.scale
404 mut y1 := (img_rect.y + img_rect.height) * ctx.scale
405 if img_rect.height == 0 {
406 scale := f32(img.width) / f32(img_rect.width)
407 y1 = f32(img_rect.y + int(f32(img.height) / scale)) * ctx.scale
408 }
409
410 flip_x := config.flip_x
411 flip_y := config.flip_y
412
413 mut u0f := if !flip_x { u0 } else { u1 }
414 mut u1f := if !flip_x { u1 } else { u0 }
415 mut v0f := if !flip_y { v0 } else { v1 }
416 mut v1f := if !flip_y { v1 } else { v0 }
417
418 // FIXME: is this okay?
419 match config.effect {
420 .alpha { sgl.load_pipeline(ctx.pipeline.alpha) }
421 .add { sgl.load_pipeline(ctx.pipeline.add) }
422 }
423
424 sgl.enable_texture()
425 sgl.texture(img.simg, img.ssmp)
426
427 if config.rotation != 0 {
428 width := img_rect.width * ctx.scale
429 height := (if img_rect.height > 0 { img_rect.height } else { img.height }) * ctx.scale
430
431 sgl.push_matrix()
432 sgl.translate(x0 + (width / 2), y0 + (height / 2), 0)
433 sgl.rotate(sgl.rad(-config.rotation), 0, 0, 1)
434 sgl.translate(-x0 - (width / 2), -y0 - (height / 2), 0)
435 }
436
437 sgl.begin_quads()
438 sgl.c4b(config.color.r, config.color.g, config.color.b, config.color.a)
439 sgl.v3f_t2f(x0, y0, config.z, u0f, v0f)
440 sgl.v3f_t2f(x1, y0, config.z, u1f, v0f)
441 sgl.v3f_t2f(x1, y1, config.z, u1f, v1f)
442 sgl.v3f_t2f(x0, y1, config.z, u0f, v1f)
443 sgl.end()
444
445 if config.rotation != 0 {
446 sgl.pop_matrix()
447 }
448
449 sgl.disable_texture()
450}
451