当前位置:网站首页>MiniGUl 1.1.0版本引入的新GDI功能和函数(二)
MiniGUl 1.1.0版本引入的新GDI功能和函数(二)
2022-06-22 06:10:00 【比特冬哥】
基于linux和 minigui的嵌入式软件开发指南
第一节:MiniGUl-Threads 和 MiniGUl-Lite 的选择
第二节:理解消息循环和窗口过程 ----------- MiniGUI编程 “ Hello World ”
第三节:对话框和控件编程
第四节:使用 GDI 函数
第五节:MiniGUl 1.1.0版本引入的新GDI功能和函数(一)
第七节:编写可移植代码
第八节:读写配置文件 与 定点数运算
文章目录
前言
我们在本系列前几节中曾经详细描述了在 MiniGUI 1.1.0版本开发过程中添加的新GDI功能和函数。这些接口首次出现在版本1.1.OPre4当中。目前
MiniGUI 1.1.0Pre7版本已经发布,该版本中的新GDI接口趋于稳定,相对1.1.0Pre4 版本而言,又新增了若干高级图形接口。这些接口涉及到直线和曲线生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接的显示缓冲区访问、YUV覆盖和 Gamma校正等等。本文将就这些主题详细描述各个接口的用法。
一、曲线和填充生成器
在一般的图形系统中,通常给用户提供若干用于进行直线或者复杂曲线,比如圆弧、椭圆和样条曲线的绘图函数。用户可以通过这些函数进行绘图,但不能利用这些系统中已有的曲线生成算法完成其他的工作。在 MiniGUI新的GDI 接口设计当中,我们采用了一种特殊的设计方法来实现曲线和封闭曲线的填充,这种方法非常灵活,而且给用户提供了直接使用系统内部算法的机会:
- 系统中定义了若干用来生成直线和曲线的函数,我们称之为"曲线生成器";
- 用户在调用生成器之前,需要定义一个回调函数,并将函数地址传递给曲线生成器,曲线生成器在生成了一个曲线上的点或者封闭曲线中的一条水平填充线时,将调用这个回调函数。
- 用户可以在回调函数当中完成针对新的点或者新的水平填充线的操作。对MiniGUI绘图函数来说,就是完成绘图工作。
- 因为回调函数在生成器的运行过程中不断调用,为了保持一致的上下文环境,系统允许用户在调用曲线生成器时传递一个表示上下文的指针,生成器将把该指针传递给回调函数。
下面将分小节讲述目前的 MiniGUI版本所提供的曲线和填充生成器。
1.1 直线剪切器和直线生成器
直线剪切器和生成器的原型如下:
/* Line clipper */
BOOL GUIAPI LineClipper (const RECT* cliprc,int *_xO,int *_y0,int *_x1,int *_y1);
/* Line generators */
typedef void (* CB_LINE) (void* context,int stepx,int stepy);
void GUIAPI LineGenerator (void* context,int x1, int y1, int x2, int y2,CB_LINE cb);
直线剪切器并不是生成器,它用于对给定的直线进行剪切操作。cliprc是给定的直线,而_xO、_y0、_xl 和_yl 传递要剪切的直线起始端点,并通过这些指针返回剪切之后的直线起始端点。MiniGUI内部使用了Cohen-Sutherland 算法。
LineGenerator是采用 Breshenham 算法的生成器。该生成器从给定直线的起始端点开始,每生成一个点调用一次 cb 回调函数,并传递上下文 context、以及新的点相对于上一个点的步进值或者差量。比如,传递stepx =1,stepy =0表示新的点比上一个点在X轴上前进一步,而在Y轴上保持不变。回调函数可以在步进值基础上实现某种程度上的优化。
1.2 圆生成器
MiniGUI定义的圆生成器原型如下:
/* Circle generator */
typedef void (* CB_CIRCLE) (void* context, int x1, int x2, int y);
void GUIAPI CircleGenerator(void* context, int sx, int sy, int r,CB_CIRCLE cb);
首先要指定圆心坐标以及半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次cb 回调函数,并传递三个值: x1、x2和 y。这三个值实际表示了圆上的两个点:(x1, y)和 (x2,y)。因为圆的对称性,生成器只要计算圆上的四分之一圆弧点即可得出圆上所有的点。
1.3 椭圆生成器
椭圆生成器和圆生成器类似,原型如下:
/* Ellipse generator * typedef void (*CB_ELLIPSE) (void* context,int x1,int x2, int y); void GUIAPI EllipseGenerator (void* context,int sx, int sy,int rx,intry,CB_ELLIPSE cb); 首先要指定椭圆心坐标以及X轴和Y轴半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次 cb 回调函数,并传递三个值: xl、x2和y。这三个值实际表示了椭圆上的两个点:(x1, y)和 (x2, y)。因为椭圆的对称性,生成器只要计算椭圆上的二分之一圆弧点即可得出椭圆上所有的点。
1.4 圆弧生成器
MiniGUI定义的圆弧生成器如下所示:
/*Arc generator */
typedef void (* CB_ARC) (void* context,int x, int y);
void GUIAPI ArcGenerator (void* context,int sx,int sy,int r,fixed angl,fixed ang2,CB_ARC cb);
首先要指定圆弧的圆心、半径、起始弧度和终止弧度。需要注意的是,起始弧度和终止弧度是采用定点数表示的,而不是浮点数,并且是弧度而不是角度。然后传递cb回调函数。每生成一个圆弧上的点,该函数将调用回调函数,并传递新点的坐标值(x,y)。
有关定点数的信息,请参阅本系列"主题六:MiniGUI提供的非GUI/GDI接口"一文。
1.5 垂直单调多边形生成器
通常而言,多边形有凸多边形和凹多边形之分。这里的垂直单调多边形,是为了优化多边形填充算法而针对计算机图形特点而提出的一种特殊多边形,这种多边形的定义如下:
垂直单调多边形是指,多边形的边和计算机屏幕上的所有水平扫描线,只能有一个或者两个交点,不会有更多交点。
图1给出了凸多边形、凹多边形和垂直单调多边形的几个示例。
需要注意的是,凸多边形一定是垂直单调多边形,但垂直单调多边形可以是凹多边形。显然,普通的多边形填充算法需要判断多边形边和每条屏幕扫描线之间的交点个数,而垂直单调多边形则可以免去这一判断,所以可以大大提高多边形填充的速度。
MiniGUI所定义的垂直单调多边形相关函数原型如下:
/*To determine whether the specified Polygon is Monotone Vertical Polygon*/
BOOL GUIAPI PolygonIsMonotoneVertical (const POINT* pts,int vertices);
* Monotone vertical polygon generator */
typedef void (*CB_POLYGON)(void* context, int x1, int x2, int y);BOOL GUIAPI MonotoneVerticalPolygonGenerator (void* context,constPOINT* pts,int vertices,CB_POLYGON cb);
PolygonIsMonotoneVertical 用来判断给定的多边形是否是垂直单调多边形,而MonotoneVerticalPolygonGenerator函数是垂直多边形生成器。在MiniGUI当中,多边形是由组成多边形的顶点来表示的。pts表示顶点数组,而vertices表示顶点个数。生成器生成的实际是填充多边形的每一条水平线,端点为(x1,y)和(x2,y)。
1.6 一般矩形生成器
MiniGUI还提供了一般的矩形生成器,该生成器可以处理凸多边形,也可以处理凹多边形。原型如下:
/* General polygon generator */
typedef void (* CB_POLYGON) (void* context,int x1, int x2, int y);BOOL GUIAPI PolygonGenerator (void* context,const POINT* pts, intvertices,CB_POLYGON cb);
和垂直单调多边形生成器一样,该函数生成的是填充多边形的每一条水平扫描线:x1是水平线的起始X坐标; x2是水平线的终止X坐标;y是水平线的Y坐标值。
1.7 填注生成器
填注(flood filling)生成器比较复杂。这个函数在 MiniGUI内部用于
FloodFill函数。我们知道,FloodFill函数从给定的起始位置开始,以给定的颜色向四面八方填充某个区域(像水一样蔓延,因此叫 Flood Filling),一直到遇到与给定起始位置的象素值不同的点为止。因此,在这一过程中,我们需要两个回调函数,一个回调函数用来判断蔓延过程中遇到的点的象素值是否和起始点相同,另外一个回调函数用来生成填充该区域的水平扫描线。在进行绘图时,该函数比较的是象素值,但实际上,该函数也可以比较任何其他值,从而完成特有的蔓延动作。这就是将填注生成器单独出来的初衷。MiniGUI如下定义填注生成器:
/* General Flood Filling generator */
typedef BOOL(*CB_EQUAL_PIXEL)(void* context, int x, int y);typedef void (* CB_FLOOD_FILL) (void* context,int x1, int x2,int y);BOOL GUIAPI FloodFillGenerator (void* context,const RECT* src_rc, intx, int y,
CB_EQUAL_PIXEL cb_equal_pixel,CB_FLOOD_FILL
cb_flood_fill);
cb_equal_pixel被调用,以便判断目标点的象素值是否和起始点一样,起始点的象素值可以通过context来传递。cb_flood_fill函数用来填充一条扫描线,传递的是水平扫描线的端点,即(x1,y)和(x2,y)。
1.8 曲线和填充生成器的用法
曲线和填充生成器的用法非常简单。为了对曲线和填充生成器有个更好的了解,我们首先看MiniGUI内部是如何使用曲线和填充生成器的。
下面的程序段来自MiniGUI的FloodFill 函数(src/newgdi/flood.c) :
static void _flood_fill_draw_hline (void* context,int x1,int x2, inty)
{
PDC pdc = (PDC)context;
RECT rcOutput = {
MIN (x1, x2),y,MAX (x1,x2)+ 1,y + 1};
ENTER_DRAWING (pdc,rcOutput);
_dc_draw_hline_clip (context,x1,x2, y);LEAVE_DRAWING (pdc,rcOutput) ;
static BO0L equal_pixel (void* context,int x, int y){
gal_pixel pixel = _dc_get_pixel_cursor ((PDC)context, x,y);
return ((PDC)context)->skip_pixel == pixel;
/* FloodFill *Fills an enclosed area (starting at point x,y).*/
BOOL GUIAPI FloodFil1 (HDC hdc,int x, int y){
PDC pdc;
BOOL ret = TRUE;
if!pde check_ecrgn (hde)))
return TRUE;口
/*hide cursor tempororily */ShowCursor (FALSE);
coor_LP2SP (pdc,&x,&y);
pdc->cur_pixel = pdc->brushcolor;pdc->cur_ban = NULL;
pdc->skip_pixel = _dc_get_pixel_cursor (pdc,x,y);
/*does the start point have a equal value? */if (pdc->skip_pixel == pdc->brushcolor)
goto equal_pixel;
ret = FloodFillGenerator (pdc,&pdc->DevRC,x, y, equal_pixel,
_flood_fill_draw_hline) ;
equal_pixel :
UNLOCK_GCRINFO (pdc);
/* Show cursor */ShowCursor (TRUE);
return ret;
}
该函数在经过一些必要的初始化工作之后,调用FloodFillGenerator函数,并传递了上下文 pdc (pdc是 MiniGUI内部表示DC的数据结构)和两个回调函数地址: equal_pixel和_flood_fill_draw_hline函数。在这之前,该函数获得了起始点的象素值,并保存在了pdc->skip_pixel当中。equal_pixel函数获得给定点的象素值,然后返回与 pdc->skip_pixel相比较之后的值;
_flood_fill_draw_hline函数调用内部函数进行水平线的绘制。
读者可以看到,这种简单的生成器实现方式,能够大大降低代码复杂度,提高代码的重用能力。有兴趣的读者可以比较MiniGUI新老GDI接口的 LineTo函数实现,相信能够得出一样的结论。
当然设计生成器的目的主要还是为方便用户使用。比如,你可以利用MiniGUI内部的曲线生成器完成自己的工作。下面的示例假定你使用圆生成器绘制一个线宽为4象素的圆:
static void draw_circle_pixel (void* context,int xl, int x2, int y)
HDC hdc = (HDC) context;
/*以圆上的每个点为圆心,填充半径为2的圆。*/FillCircle (hdc,x1,y,2);
FillCircle (hdc,x2,y,2);
void DrawMyCircle (HDC hdc,int x, int y, int r, gal_pixel pixel)
gal_pixel old_brush;
old_bursh = SetBrushColor (hdc,pixle);
/*调用圆生成器*/
CircleGenerator ((void*) hdc,x,y,r,draw_circle_pixel);
/*恢复旧的画刷颜色*/
SetBrushColor (hdc,old_brush) ;
从上面的例子可以看出,曲线和填充生成器的用法极其简单,而且结构清晰明了。读者在自己的开发过程中,也可以学习这种方法。
二、绘制复杂曲线
基于上段中描述的曲线生成器,MiniGUI提供了如下基本的曲线绘制函数:
void GUIAPI MoveTo (HDC hdc,int x, int y);void GUIAPT LineTo (HDC hdc,int x, int y);
void GUIAPI Rectangle (HDC hdc,int x0, int y0, int xl, int y1);
void GUIAPI PollyLineTo (HDC hdc,const POINT* pts,int vertices);
void GUIAPI SplineTo (HDC hdc,const POINT* pts);
void GUIAPI Circle (HDC hdc,int sx,int sy,int r);
void GUIAPI Ellipse (HDC hdc,int sx, int sy,int rx, int ry);
void GUIAPI Arc (HDC hdc,int sx,int sy,int r, fixed angl,fixed ang2);

作为示例,我们看Circle和 Ellipse函数的用法。假定给定了两个点,pts[0]和 pts[1],其中 pts[0]是圆心或者椭圆心,而 pts[1]是圆或者椭圆外切矩形的一个顶点。下面的程序段绘制由这两个点给定的圆或者椭圆:
三、封闭曲线填充
MiniGUI目前提供了如下的封闭曲线填充函数:
void GUIAPI FillBox (HDC hdc,int x,int y, int w,int h);
void GUIAPI Fil1Circle (HDC hdc,int sx,int sy,int r);
void GUIAPI FillEllipse (HDC hdc,int sx,int sy, int rx,int ry);
void GUIAPI FillSector (HDC hdc,int sx,int sy,int r, int angl,int ang2);
BOOL GUIAPI FillPolygon (HDC hdc,const POINT* pts,int vertices);
BOOL GUIAPI FloodFill (HDC hdc,int x,int y);

需要注意的是,所有填充函数使用当前画刷属性(颜色),并且受当前光栅操作的影响。
下面的例子说明了如何使用FillCircle 和 FillEllipse 函数填充圆或者椭圆。假定给定了两个点,pts[0]和 pts[1],其中 pts[0]是圆心或者椭圆心,而pts[1]是圆或者椭圆外切矩形的一个顶点。
int rx = ABS (pts[1].x - pts[o].x);int ry = ABS (pts[1].y - pts[0].y);
if (rx == ry)
FillCircle (hdc,pts[o].x,pts[o].y,rx);else
FillEllipse (hdc,pts[O].x, pts[o].y,rx, ry);
四、建立复杂区域
除了利用填充生成器进行填充绘制以外,我们还可以使用填充生成器建立由封闭曲线包围的复杂区域。我们知道,MiniGUI当中的区域是由互不相交的矩形组成的,并且满足x-y-banned 的分布规则。利用上述的多边形或者封闭曲线生成器,可以将每条扫描线看成是组成区域的高度为1的一个矩形,这样,我们可以利用这些生成器建立复杂区域。MiniGUI利用现有的封闭曲线生成器,实现了如下的复杂区域生成函数:
BOOL GUIAPI InitCircleRegion (PCLIPRGN dst, int x, int y, int r);
BOOL GUIAPI InitEllipseRegion (PCLIPRGN dst, int x, int y, int rx, intry);
BOOL GUIAPI InitPolygonRegion (PCLIPRGN dst,const POINT* pts, intvertices);
BOOL GUIAPI InitSectorRegion_(PCLIPRGN dst,const POINT* pts,intvertices) :
利用这些函数,我们可以将某个区域分别初始化为圆、椭圆、多边形和扇形区域。然后,可以利用这些区域进行点击测试(PtInRegion和 RectInRegion),或者选择到DC当中作为剪切域,从而获得特殊显示效果。
五、直接访问显示缓冲区
在新的GDI接口中,我们添加了用来直接访问显示缓冲区的函数,原型如下:
Uint8* GUIAPI LockDC (HDC hdc,const RECT* rw_rc,int* width,int* height,int* pitch);
void GUIAPI UnlockDC (HDC hdc);

锁定一个HDC意味着MiniGUI进入以互斥方式访问显示缓冲区的状态。如果被锁定的HDC是一个屏幕DC(即非内存DC),则该函数将在必要时隐藏鼠标光标,并锁定HDC对应的全局剪切域。在锁定一个 HDC之后,程序可通过该函数返回的指针对锁定区域进行访问。需要注意的是,不能长时间锁定一个 HDC,也不应该在锁定一个HDC时进行其他额外的系统调用。
假定以锁定矩形左上角为原点建立坐标系,X轴水平向右,Y轴垂直向下,则可以通过如下的公式计算该坐标系中(x,y)点对应的缓冲区地址(假定该函数返回的指针值为frame_buffer) :
Uint8* pixel_add = frame_buffer + y *(*pitch)+ x * GetGDCapability(hdc,GDCAP_BPP);
根据该 HDC 的颜色深度,就可以对该象素进行读写操作。作为示例,下面的程序段随机填充锁定区域:
int i, width,height,pitch;RECT rc = {
0,0,200,200} ;
int bpp =GetGDCapability (hdc,GDCAP_ BPP);
Uint8* frame_buffer =LockDC (hdc,&rc,&width, &height,&pitch);
Uint8* row = frame_buffer;
for (i = 0; i< *height; i++)
{
memset (row,rand () %0x100,*width * bpp);
row += *pitch;
}
UnlockDC (hdc);
六、YUV覆盖和 Gamma校正
为了增强 MiniGUI对多媒体的支持,我们增加了对YUV覆盖(Overlay)和Gamma校正的支持。
6.1 YUV覆盖(Overlay)
多媒体领域中,尤其在涉及到MPEG播放时,通常使用YUV颜色空间来表示颜色,如果要在屏幕上显示一副MPEG解压之后的图片,则需要进行YUV颜色空间到RGB颜色空间的转换。YUV覆盖最初来自一些显示芯片的加速功能。这种显示芯片能够在硬件基础上完成YUV到RGB的转换,免去软件转换带来的性能损失。在这种显示芯片上建立了YUV覆盖之后,可以直接将YUV信息写入缓冲区,硬件能够自动完成YUV到RGB的转换,从而在 RGB显示器上显示出来。在不支持YUⅣ覆盖的显示芯片上,MiniGUI也能够通过软件实现 YUⅣ覆盖,这时,需要调用DisplayYUVOverlay函数将YUV信息转换并缩放显示在建立 YUV覆盖的DC设备上。
MiniGUI 提供的 YUV覆盖操作函数原型如下:
/***************************** YUV overlay support***************************/
/*最常见的视频覆盖格式.*/
#define GAL_YV12_OVERLAY 0x32315659/* Planar mode: Y + V+ U (3planes)*/
#define GAL_IYUV_OVERLAY Ox56555949/* Planar mode: Y + U +V (3planes)*/
#define GAL_YUY2_OVERLAY Ox32595559
/*Packed mode: YO+UO+Y1+VO (1 plane)*/
#define GAL_UYVY_OVERLAY 0x59565955
/*Packed mode: UO+YO+VO+Y1 (1 plane)*/
#define GAL_YVYU_OVERLAY Ox55595659
/*Packed mode: YO+VO+Y1+UO (1 plane)*/
/*该函数创建一个视频输出覆盖*/
GAL_Overlay* GUIAPI CreateYUVOverlay (int width,int height,
Uint32 format,HDC hdc) ;
/*锁定覆盖进行直接的缓冲区读写,结束后解锁*/int GAL_LockYUVOverlay (GAL_Overlay *overlay);void GAL_UnlockYUVOverlay (GAL_Overlay *overlay) ;
#define LockYUVOverlay GAL_LockYUVOverlay#define UnlockYUVOverlay GAL_UnlockYUVOverlay
/*释放视频覆盖*/
void GAL_FreeYUVOverlay (GAL_Overlay *overlay);#define FreeYUVOverlay GAL_FreeYUVOverlay
/*将视频覆盖传送到指定DC设备上。该函数能够进行2维缩放 */
void GUIAPI DisplayYUVOverlay (GAL_Overlay* overlay,const RECT*dstrect) ;
有关视频格式的信息,可参见:
http: / /www.webartz.com/fourcc/indexyuv.htm
有关颜色空间的相互关系的息,可参见:
http: //www.neuro.sfc. keio. ac. jp/~aly/polygon/info/color-space-faq.htm
6.2 Gamma校正
Gamma校正通过为RGB 颜色空间的每个颜色通道设置Gamma因子,来动态调整RGB显示器上的实际 RGB效果。需要注意的是,Gamma校正需要显示芯片的硬件支持。
应用程序可以通过SetGamma函数设置RGB三个颜色通道的Gamma校正值。该函数原型如下:
int GAL_SetGamma (float red,float green,float blue);
#define SetGamma GAL_SetGamma
线性Gamma校正值的范围在0.1到10.0之间。如果硬件不支持Gamma校正,该函数将返回—1。
应用程序也可以通过SetGammaRamp函数设置RGB 三个颜色通道的非线性Gamma校正值。该函数原型如下:
int GAL_SetGammaRamp (Uint16 *red,Uint16 *green,Uint16 *blue);
#define SetGammaRamp GAL SetGammaRamp
int GAL_GetGammaRamp (Uint16 *red,Uint16 *green,Uint16 *blue);
#define GetGammaRamp GAL_GetGammaRamp
函数SetGammaRamp 实际设置的是每个颜色通道的Gamma转换表,每个表由256个值组成,表示设置值和实际值之间的对应关系。当设置屏幕上某个象素的RGB分别为R、G、B时,实际在显示器上获得的象素 RGB值分别为: red[R]、green[G]、blue[B]。如果硬件不支持Gamma校正,该函数将返回-1。
函数GetGammaRamp获得当前的Gamma 转换表。
Gamma校正的最初目的,是为了能够在显示器上精确还原一副图片。Gamma值在某种程度上表示的是某个颜色通道的对比度变化。但Gamma在多媒体和游戏程序中有一些特殊用途――通过Gamma校正,可以方便地获得对比度渐进效果。
总结
本文描述了自MiniGUI 1.1.0Pre4版本发布以来新增的GDI接口。这些接口涉及到曲线和填充生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接访问FrameBuffer、YUV覆盖和Gamma校正等等。通过本文的介绍,相信读者能够对MiniGUI 的新GDI接口有一个更加全面的认识。
边栏推荐
- 上传文件提示 413 Request Entity Too Large 错误
- Research on dynamics and control of single ball robot
- 为什么我选择 Rust
- Current harmonic suppression strategy of grid connected inverter with PIR controller regulator
- 生信可视化(part2)--箱线图
- 线程池的七大参数及自定义线程池
- 虚职、架空、拖后腿,大厂开源办公室到底什么样?
- 生信可视化(part1)--柱状图
- Machine learning concept sorting (no formula)
- 【自己动手写CPU】异常相关指令的实现
猜你喜欢

生信可视化(part3)--小提琴图

单细胞论文记录(part14)--CoSTA: unsupervised convolutional neural network learning for ST analysis

富设备平台突破:基于RK3568的DAYU200进入OpenHarmony 3.1 Release主干

C#中的泛型

四大函数式接口(必需掌握)

No business series 7: removing spots from old photos

402-字符串(题目:剑指Offer58-II.左旋转字符串、 28. 实现 strStr()、459.重复的子字符串)

电热水壶坏了别扔,它很容易修好的!

Le contrôle MFC tabctrl modifie la taille de l'étiquette

【CPU设计实战】数字逻辑电路设计基础(一)
随机推荐
D3D10 screenshot function saves texture to local
单细胞文献学习(part3)--DSTG: deconvoluting spatial transcriptomics data through graph-based AI
Array and foreach traversal in C #
【CPU设计实战】数字逻辑电路设计基础(一)
Surfer格网文件裁剪
信息系统项目管理 - 范围管理(划重点)
基于卫星测深的牙买加沿岸水深测量
Research on automatic landing control system of carrier aircraft
h = key. Hashcode()) ^ (H > > 16) detailed explanation and why the hashcode value should be shifted to the right by 16 bits and XOR with the original hashcode value
【Rust笔记】03-引用
TiDB 社区线下交流会,天津 & 石家庄的小伙伴看过来~
idea插件EasyCode的使用
reduce_sum()中的reduction_indices
Creating GLSL Shaders at Runtime in Unity3D
Machine learning concept sorting (no formula)
Callable
Markdown中插入类图(classDiagram)
[technical notes]
单细胞论文记录(part6)--SpaGE: Spatial Gene Enhancement using scRNA-seq
Discrete PID control based on MATLAB