Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1393695 Posts in 67098 Topics- by 60052 Members - Latest Member: lefter33

August 01, 2021, 03:12:02 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperArt (Moderator: JWK5)Workflow of pixel art meant for CRT display
Pages: [1]
Print
Author Topic: Workflow of pixel art meant for CRT display  (Read 1810 times)
diegzumillo
Level 10
*****


This avatar is so old I still have a some hair


View Profile WWW
« on: March 29, 2021, 05:44:47 AM »

HEy all

If you are making art meant for a specific type of display, ideally you would have the display at hand to preview it while working. Unfortunately that's no good for me, I need a good emulation on my screen, either in the program itself or external program etc. Does anyone have any experience with this? any suggestions?

My current solution is inconvenient. I'm making a Mega Drive game, so I use an emulator with a good filter that imitates the visual I'm after. So in order to preview my work I have to export the art, recompile the game, and launch it in the emulator.

My editor of choice is Aseprite; I would really love to have a small preview right there.
Logged

0rel
Level 4
****


View Profile
« Reply #1 on: April 01, 2021, 05:11:25 AM »

With SDL and some patience, it would be possible to clobber such a little program together rather quickly. Detect when the file changed, then automatically reload it, and display it with a CRT filter applied to it, borrowed from an emulator... make the window stay on top, by preference with no borders and voilà. But, is it really worth it? It always takes longer than expected... - Why do you not hook up a real CRT as a second monitor, the work in progress sprite could be reloaded whenever saved in a normal image viewer. HDMI to VGA/composite adapters exist and are not that expensive, it would would probably even work on a portable CRT as well... not sure.

However, I've found this project on GitHub after a quick search: crtview. There are probably other tools that do exactly that as well.

Quick mod: To make the window stay on top (on Windows), and reload the file with the R key, this patch can be applied:
Code:
diff --git a/source/app.h b/source/app.h
index eb0ff38..952480d 100644
--- a/source/app.h
+++ b/source/app.h
@@ -1850,6 +1850,10 @@ static LRESULT CALLBACK app_internal_wndproc( HWND hwnd, UINT message, WPARAM wp
                         app_screenmode( app, APP_SCREENMODE_WINDOW );
                     }
                 }
+                else {
+                            if(IsWindowVisible(app->hwnd))
+                                SetWindowPos(app->hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
+                }

             if( app->clip_cursor )
                 {
diff --git a/source/main.c b/source/main.c
index c72fd12..c3349ee 100644
--- a/source/main.c
+++ b/source/main.c
@@ -61,6 +61,12 @@ int app_proc( app_t* app, void* user_data ) {
                 else if( input.events->data.key == APP_KEY_DOWN ) {
                     newmode = ( mode + 1 ) % 3;
                 }
+                else if (input.events->data.key == APP_KEY_R) {
+                    xbgr = filename ? (APP_U32*) stbi_load( filename, &w, &h, &c, 4 ) : NULL;
+                    if( !xbgr ) {
+                        xbgr = ␣
+                    }
+                }
             }
         }
Logged
diegzumillo
Level 10
*****


This avatar is so old I still have a some hair


View Profile WWW
« Reply #2 on: April 01, 2021, 01:55:16 PM »

Interesting! this project compiled just fine here and it looks nice. But you're right, making this part of my workflow will definitely take time, and I don't know if it's worth it.
Logged

0rel
Level 4
****


View Profile
« Reply #3 on: April 01, 2021, 07:58:46 PM »

Nice, it might be worth it if you plan to make lots of pixel art, for one single project. Played some real SNES on real CRT recently again, and CRTs are in fact different, and pixel really have completely different flavor, so previewing it is useful I think. - Also played Xeno Crisis quite a bit recently (on PC), and was impressed, great game... to use old tech is not really a limitation...

aseprite is open source BTW and it already has a preview window. So it would probably be best to try to add such a CRT filter feature there, might be quite useful, since it is also capable of previewing animations.

That CRTview program is a bit of a mess, found it randomly yesterday, and assumed it uses a pixel based filter, but instead OpenGL with an GLSL shader is used to emulate the CRT effect. The scanlines/pixel grid does not match up with the actual pixels in the image, it would need some improvements to adjust the filter to the actual resolution, resize the window accordingly etc. to be actually useful I guess. If you work with a fixed screen resolution however, it could be modified to work with just that resolution...

Butchered it some more just for fun:
- crude auto file reload function added
- CRT frame removed
- curved display removed
- fullscreen mode disabled
- title changed
- fixed window size
...

Test image: https://www.spriters-resource.com/resources/sheets/45/47825.png:


...displayed in modified CRTview:


mod0.patch (contains obsolete WinAPI stuff and uses a fixed window size...):
Code:
diff --git a/source/app.h b/source/app.h
index eb0ff38..113c573 100644
--- a/source/app.h
+++ b/source/app.h
@@ -23,6 +23,9 @@ before you include this file in *one* C/C++ file to create the implementation.
     #define APP_U64 unsigned long long
 #endif
 
+#define WIN_STYLE (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX)
+#define WIN_WIDTH 288 //564 //256
+#define WIN_HEIGHT 304 //332 //256
 
 typedef struct app_t app_t;
 
@@ -1849,6 +1852,9 @@ static LRESULT CALLBACK app_internal_wndproc( HWND hwnd, UINT message, WPARAM wp
                     if( cr.right - cr.left != app->fullscreen_width || cr.bottom - cr.top != app->fullscreen_height )
                         app_screenmode( app, APP_SCREENMODE_WINDOW );
                     }
+                } else {
+                    if (IsWindowVisible(app->hwnd))
+                        SetWindowPos(app->hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
                 }
 
             if( app->clip_cursor )
@@ -2033,8 +2039,8 @@ int app_run( int (*app_proc)( app_t*, void* ), void* user_data, void* memctx, vo
     if( app->display_count <= 0 ) { app_log( app, APP_LOG_LEVEL_ERROR, "Failed to get display info" ); goto init_failed;  }
 
     // Setup the main application window
-    app->windowed_w = app->displays[ 0 ].width - app->displays[ 0 ].width / 6;
-    app->windowed_h = app->displays[ 0 ].height - app->displays[ 0 ].height / 6;
+    app->windowed_w = WIN_WIDTH; //app->displays[ 0 ].width - app->displays[ 0 ].width / 6;
+    app->windowed_h = WIN_HEIGHT; //app->displays[ 0 ].height - app->displays[ 0 ].height / 6;
     app->windowed_x = ( app->displays[ 0 ].width - app->windowed_w ) /  2;
     app->windowed_y = ( app->displays[ 0 ].height - app->windowed_h ) / 2;
 
@@ -2050,7 +2056,7 @@ int app_run( int (*app_proc)( app_t*, void* ), void* user_data, void* memctx, vo
     wc.hInstance = app->hinstance; wc.hIcon = app->icon; wc.hCursor = app->current_pointer;
     wc.hbrBackground = (HBRUSH) GetStockObject( BLACK_BRUSH ); wc.hIconSm = app->icon;
     RegisterClassEx( &wc );
-    app->hwnd = CreateWindowEx( 0, wc.lpszClassName, 0, WS_OVERLAPPEDWINDOW, app->windowed_x, app->windowed_y,
+    app->hwnd = CreateWindowEx( 0, wc.lpszClassName, 0, WIN_STYLE, app->windowed_x, app->windowed_y,
         winrect.right - winrect.left, winrect.bottom - winrect.top, (HWND) 0, (HMENU) 0, app->hinstance, 0 );
     if( !app->hwnd ) { app_log( app, APP_LOG_LEVEL_ERROR, "Failed to create window." ); goto init_failed; }
     app->hdc = GetDC( app->hwnd );
@@ -2739,7 +2745,7 @@ void app_screenmode( app_t* app, app_screenmode_t screenmode )
     BOOL visible = IsWindowVisible( app->hwnd );
     if( screenmode == APP_SCREENMODE_WINDOW )
         {
-        SetWindowLong( app->hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW | ( visible ? WS_VISIBLE : 0 ) );
+        SetWindowLong( app->hwnd, GWL_STYLE, WIN_STYLE | ( visible ? WS_VISIBLE : 0 ) );
 
         WINDOWPLACEMENT placement;
         placement.length = sizeof( placement );       
@@ -2750,6 +2756,7 @@ void app_screenmode( app_t* app, app_screenmode_t screenmode )
         placement.rcNormalPosition.top = app->windowed_y;
         placement.rcNormalPosition.right = app->windowed_x + app->windowed_w;
         placement.rcNormalPosition.bottom = app->windowed_y + app->windowed_h;
+        AdjustWindowRect (&placement.rcNormalPosition, GetWindowLong(app->hwnd, GWL_STYLE), FALSE);
         SetWindowPlacement( app->hwnd, &placement );
         }
     else
diff --git a/source/crtemu_pc.h b/source/crtemu_pc.h
index 1430cf7..9c555cf 100644
--- a/source/crtemu_pc.h
+++ b/source/crtemu_pc.h
@@ -596,10 +596,11 @@ crtemu_pc_t* crtemu_pc_create( void* memctx )
         "\n"
         "vec3 tsample( sampler2D samp, vec2 tc, float offs, vec2 resolution )\n"
      "    {\n"
-     "    tc = tc * vec2(1.035, 0.96) + vec2(-0.0125*0.75, 0.02);\n"
+     "    /*tc = tc * vec2(1.035, 0.96) + vec2(-0.0125*0.75, 0.02);\n"
  " tc = tc * 1.2 - 0.1;\n"
      "    vec3 s = pow( abs( texture2D( samp, vec2( tc.x, 1.0-tc.y ) ).rgb), vec3( 2.2 ) );\n"
-     "    return s*vec3(1.25);\n"
+     "    return s*vec3(1.25);*/\n"
+        "     return texture2D(samp, vec2( tc.x, 1.0-tc.y )).rgb;\n"
      "    }\n"
         "\n"
         "vec3 filmic( vec3 LinearColor )\n"
@@ -627,9 +628,9 @@ crtemu_pc_t* crtemu_pc_create( void* memctx )
         "void main(void)\n"
  " {\n"
      "    /* Curve */\n"
-     "    vec2 curved_uv = mix( curve( uv ), uv, 0.8 );\n"
+     "    vec2 curved_uv = uv; //mix( curve( uv ), uv, 0.8 );\n"
      "    float scale = 0.04;\n"
-     "    vec2 scuv = curved_uv*(1.0-scale)+scale/2.0+vec2(0.003, -0.001);\n"
+     "    vec2 scuv = uv; //curved_uv*(1.0-scale)+scale/2.0+vec2(0.003, -0.001);\n"
         "\n"
      "    /* Main color, Bleed */\n"
      "    vec3 col;\n"
@@ -667,7 +668,7 @@ crtemu_pc_t* crtemu_pc_create( void* memctx )
      "    /* Vignette */\n"
         "    float vig = (0.1 + 1.0*16.0*curved_uv.x*curved_uv.y*(1.0-curved_uv.x)*(1.0-curved_uv.y));\n"
      "    vig = 1.3*pow(vig,0.5);\n"
-     "    col *= vig;\n"
+     "    //col *= vig;\n"
      "\n"
      "    /* Scanlines */\n"
      "    float scans = clamp( 0.35+0.18*sin(0.0*time+curved_uv.y*resolution.y*1.5), 0.0, 1.0);\n"
@@ -701,8 +702,8 @@ crtemu_pc_t* crtemu_pc_create( void* memctx )
      "    vec4 f=texture2D(frametexture, fuv * vec2(0.91, 0.8) + vec2( 0.050, 0.093 ));\n"
      "    f.xyz = mix( f.xyz, vec3(0.5,0.5,0.5), 0.5 );\n"
      "    float fvig = clamp( -0.00+512.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y), 0.2, 0.85 );\n"
- " col *= fvig;\n"
-     "    col = mix( col, mix( max( col, 0.0), pow( abs( f.xyz ), vec3( 1.4 ) ), f.w), vec3( use_frame) );\n"
+ " //col *= fvig;\n"
+     "    //col = mix( col, mix( max( col, 0.0), pow( abs( f.xyz ), vec3( 1.4 ) ), f.w), vec3( use_frame) );\n"
         "    \n"
  " gl_FragColor = vec4( col, 1.0 );\n"
  " }\n"
@@ -1050,18 +1051,18 @@ void crtemu_pc_present( crtemu_pc_t* crtemu_pc, CRTEMU_PC_U64 time_us, CRTEMU_PC
     int window_width = viewport[ 2 ] - viewport[ 0 ];
     int window_height = viewport[ 3 ] - viewport[ 1 ];
 
-    window_width = (int)( window_width / 1.2f );
+    window_width = window_width; //(int)( window_width / 1.2f );
 
     float hscale = window_width / (float) width;
-    float vscale = window_height / ( (float) height * 1.1f );
+    float vscale = window_height / ( (float) height); // * 1.1f );
     float pixel_scale = hscale < vscale ? hscale : vscale;
 
-    float hborder = ( window_width - pixel_scale * width ) / 2.0f;
-    float vborder = ( window_height - pixel_scale * height * 1.1f ) / 2.0f;
+    float hborder = 0.0f; // ( window_width - pixel_scale * width ) / 2.0f;
+    float vborder = 0.0f; // ( window_height - pixel_scale * height * 1.1f ) / 2.0f;
     float x1 = hborder;
     float y1 = vborder;
     float x2 = x1 + pixel_scale * width;
-    float y2 = y1 + pixel_scale * height * 1.1f;
+    float y2 = y1 + pixel_scale * height; // * 1.1f;
 
     x1 = ( x1 / window_width ) * 2.0f - 1.0f;
     x2 = ( x2 / window_width ) * 2.0f - 1.0f;
diff --git a/source/main.c b/source/main.c
index c72fd12..6e3ff18 100644
--- a/source/main.c
+++ b/source/main.c
@@ -8,10 +8,21 @@
 #include "crt_frame_pc.h"
 #include "stb_image.h"
 
+#include <sys/stat.h>
+
+static time_t _get_file_mtime(const char *filename)
+{
+    time_t ret = 0;
+    struct stat file_info;
+    if (stat (filename, &file_info) == 0) {
+        ret = file_info.st_mtime;
+    }
+    return ret;
+}
 
 int app_proc( app_t* app, void* user_data ) {
     char const* filename = (char const*) user_data;
-    app_title( app, "CRTView - UP/DOWN F11 ESC" );
+    app_title( app, "CRTView [mod0]"); // - UP/DOWN F11 ESC" );
     app_interpolation( app, APP_INTERPOLATION_NONE );
     app_screenmode_t screenmode = APP_SCREENMODE_WINDOW;
     app_screenmode( app, screenmode );
@@ -37,6 +48,7 @@ int app_proc( app_t* app, void* user_data ) {
         xbgr = &blank;
     }
 
+    time_t file_mtime = _get_file_mtime(filename);
     APP_U64 start = app_time_count( app );
 
     int mode = 0;
@@ -52,7 +64,7 @@ int app_proc( app_t* app, void* user_data ) {
                 if( input.events->data.key == APP_KEY_ESCAPE ) {
                     exit = 1;
                 }
-                else if( input.events->data.key == APP_KEY_F11 ) {
+                /*else if( input.events->data.key == APP_KEY_F11 ) {
                     screenmode = screenmode == APP_SCREENMODE_WINDOW ? APP_SCREENMODE_FULLSCREEN : APP_SCREENMODE_WINDOW;
                     app_screenmode( app, screenmode );
                 } else if( input.events->data.key == APP_KEY_UP ) {
@@ -60,7 +72,7 @@ int app_proc( app_t* app, void* user_data ) {
                 }
                 else if( input.events->data.key == APP_KEY_DOWN ) {
                     newmode = ( mode + 1 ) % 3;
-                }
+                }*/
             }
         }
 
@@ -94,7 +106,17 @@ int app_proc( app_t* app, void* user_data ) {
         } else {
             app_present( app, xbgr, w, h, 0xffffff, 0x181818 );
         }
-    }
+
+        static APP_U64 _tprev = 0;
+        if (t > _tprev + 1000000) {
+            _tprev = t;
+            time_t mtime = _get_file_mtime(filename);
+            if (file_mtime != mtime) {
+                file_mtime = mtime;
+                xbgr = (APP_U32*) stbi_load( filename, &w, &h, &c, 4 ); /* reload file */
+            }
+        }
+      }
 
     if( mode == 0 ) {
         crtemu_pc_destroy( crtemu_pc );
@@ -141,7 +163,12 @@ int main( int argc, char** argv ) {
         #endif
     #endif
 
-    char* filename = argc > 1 ? argv[ 1 ] : NULL;
+    if (argc <= 1) {
+        printf("usage: %s <filename>", argv[0]);
+        return 0;
+    }
+
+    char* filename = argv[1];
     return app_run( app_proc, filename, NULL, NULL, NULL );
 }
 


To apply the patch, run (make sure to also copy-paste the newline at the end of the file and use LF instead of CR+LF line endings):
Code:
$ git clone https://github.com/mattiasgustavsson/crtview
$ cd crtview
$ git apply mod0.patch

And to test (in my case, on Windows 10 with Visual Studio 2019):
Code:
$ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
$ cl source\main.c && main.exe test.png
« Last Edit: April 04, 2021, 05:52:03 AM by 0rel » Logged
diegzumillo
Level 10
*****


This avatar is so old I still have a some hair


View Profile WWW
« Reply #4 on: April 02, 2021, 03:25:49 AM »

haha it's funny you mention that about aseprite. See, I've been considering software development as a plan B in my life, (plan A is not working out) and one of my tasks to start on that path is to contribute to an open source project. My first thought was to add CRT filter to the preview window of aseprite.

I still have a few more weeks of plan A though Tongue
Logged

0rel
Level 4
****


View Profile
« Reply #5 on: April 02, 2021, 10:13:37 PM »

Quote
My first thought was to add CRT filter to the preview window of aseprite.

That would be a useful addition. Aseprite's source looks very well organized and clean, but also quite advanced and huge. And I'm not sure if it is really a collaborative project and that open for contributions, it gets released under different licenses:
https://github.com/aseprite/aseprite#license
https://www.aseprite.org/faq/
https://dev.aseprite.org/2016/09/01/new-source-code-license/

Code: (https://github.com/aseprite/aseprite/blob/main/EULA.txt)
(g) Source code.
You may only compile and modify the source code of the SOFTWARE PRODUCT
for your own personal purpose or to propose a contribution to the SOFTWARE PRODUCT.

To make a CRT preview utility as a little standalone utility would be a lot easier IMHO (nice to have features that come to mind: adjustable CRT filter parameters via config file, sprite/tile animations, move pixels to see different CRT pixel alignment artifacts, a few shortcuts to zoom 2/3/4/5x, turn filter on/off, play/stop/step the animation).

(To glue in a blur/scanline filter to a personal build of aseprite shouldn't take too long though, I guess... Some spots where a preview filter could possibly be placed for testing (not really clear to me if this is the right place to start. Building the project on Windows is already a challenge, I don't fiddle around with this anymore right now :X): https://github.com/aseprite/aseprite/blob/main/src/render/render.cpp, line 753, 1261)


Quote
I still have a few more weeks of plan A though

Good luck to you! Is the Mega Drive game you've mentioned this one in the DevLogs? That looks awesome, really dig the animations and the aesthetics (it reminds me a bit of Flashback, need to read more on the gameplay later on). Keep it up! These are tough times...
Logged
diegzumillo
Level 10
*****


This avatar is so old I still have a some hair


View Profile WWW
« Reply #6 on: April 03, 2021, 09:55:08 AM »

Quote
Good luck to you! Is the Mega Drive game you've mentioned this one in the DevLogs? That looks awesome, really dig the animations and the aesthetics (it reminds me a bit of Flashback, need to read more on the gameplay later on). Keep it up! These are tough times...

Thanks! But no, plan A is a career in science. Apparently that's not going to work in the current situation of the world/my country. Plan B is software dev because as a physicist I can more or less code and understand math. I have friends who made the transition, so it's possible.

Game dev is my current and always hobby!

Maybe I'll give this CRt project a shot eventually.
Logged

0rel
Level 4
****


View Profile
« Reply #7 on: April 03, 2021, 01:29:41 PM »

Quote
Game dev is my current and always hobby!

Sorry for the misunderstanding Embarrassed But good to hear that, since indie games blew up in so many directions.


Quote
Plan B is software dev because as a physicist I can more or less code and understand math. I have friends who made the transition, so it's possible.

Yes there's always plenty of interesting work in software, I've also switched over to code and some hardware design too after thinking game dev would be a viable route. Nope. (I'm just trying to get into experimental hobby game dev again at some point...)


Quote
Maybe I'll give this CRt project a shot eventually.

Tools can be huge time sink, thinking back, I've spent way too much time working on my own pre-unity framework code and fiddling with some tools instead of the actual gameplay and content. - So, for the CRT preview, I'd try to just hook up an old PC CRT monitor and use Aseprite's own preview window on a second monitor, and focus on the art assets, and integrate some hardware into the workflow Smiley

But as a contribution to an open-source project, such a feature might be a good candidate since common pixel graphics tools apparently lack such a more or less obscure feature. Gimp has a animation preview window and Krita handles animations nicely, but doesn't have a preview window, might be another project that people use for pixel work... Aseprite is a commercial project as far as I know, so it's probably a bit more difficult to contribute there, just guessing.
« Last Edit: April 03, 2021, 01:36:40 PM by 0rel » Logged
Beastboy
Level 2
**

Happy birth day mom!


View Profile
« Reply #8 on: April 05, 2021, 03:07:52 PM »

I love contra hardcorpse, that boss was amazing when i first encounter it in the junkyard map. I played contra on console and on emulator and i prefert the old school tv visuals but both are nice
Logged
Schoq
Level 10
*****


♡∞


View Profile WWW
« Reply #9 on: April 06, 2021, 04:16:28 AM »

This might not work for every kind of art brain, but it should definitely be good enough to just make some art that you check through a filter for a while to take mental notes of how the characteristics change on the pixel level and then work with that in mind.
If you're going for a genuine old school look, these megadrive artists on a schedule rarely obsessed that much over precise pixel arrangement. Not the way modern pixel artists do with our matrix of crisp squares and all the theoretic advances on places like pixelation.org.
Additionally, "mistakes" will be somewhat obfuscated anyway.
For these reasons, there aren't really any CRT-specific techniques to unlock for the purpose of looking like a game from the 90s.
Logged

♡ ♥ make games, not money ♥ ♡
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic