Skip to content

This plugin adds GNUPlot terminal to the Godot engine for displaying plots on a 2D canvas layer without requiring the GNUPlot executable

License

Notifications You must be signed in to change notification settings

dmrokan/gdmatplot

Repository files navigation

GDMatPlot

This native Godot extension embeds GNUPlot command line into the game engine by exploiting GNUPlot's flexibility for declaring custom graphical output terminals. You can create static or dynamic plots for games or GUI applications and it does not require GNUPlot executable. Plots can be embedded into 3D world by using sub viewports. It can plot custom dataframes defined in scripts. Also, you can provide a PackedFloat64Array as dataframe which is a more performant method. Please, see examples in demo folder.

How it works

I created a custom graphical terminal named by gdmp.trm which redirects GNUPlot's drawing calls to Godot's CanvasItem drawing methods. gdmp.trm is based on SVG terminal implemented in gnuplot/term/svg.trm. The common method to embed GNUPlot into an application is running it as a separate process and communicating through an Inter Process Communication (IPC) method. However, I came up with a hack to embed it into the plugin itself as a separate shared library after some tedious work. GDMP terminal and mentioned hack can be found in gdmatplot-gnuplot.patch.

The main issue that can not be solved programmatically is handling the global program state used by GNUPlot while drawing multiple plots concurrently. The common method to load a shared library on Linux is reading it by dlopen. If you load the same dynamic library with dlopen several times all symbols will be allocated at the same memory address (similar to global variables in the same namespace) in the application. Changing a global symbol in a plot will effect the others and cause race conditions in a threaded application. GNU linker on Linux provides a function called dlmopen which overcomes this problem up to a point by separating subsequent library loads into separate memory regions. However, it supports a maximum of 16 different namespaces and it is not a multiplatform solution. After some research and trial-error, found that fooling the dynamic library loader by changing dynamic library file name each time before loading makes the loader create separate GNUPlot instances. This method seems like working for both Linux and Windows.

Build steps of GNUPlot is declared in SConscript. It first compiles GNUPlot source as a shared library, then dumps it into a C array to be packed in GDMatPlot extension. The C array written to a '.so' or '.dll' file to be read by dynamic library loader and deleted afterwards.

Beta warning

As explained above, this software is based on a hack that drags it into a indefinitely long beta stage despite it is based on GNUPlot's 6.0.0 release. In the tests provided in demo projects, I didn't experience illegal memory access or memory leak. However, it is not easy to cover all code paths. Users of this library should notice the comments below.

Known (but not limited) regressions

  • Dashed plot types is not working
  • Unicode text is not working
  • Can not change font family by using GNUPlot font commands
  • Arrow plots are not working
  • Spider plots are not working
  • Command line breaks by using backslash character is not working
  • Clean termination after error is not working
  • Z-ordering of line segments in 3D plots is not working
  • Can not load dataframes from files, GNUPlot's I/O functionality is disabled

Note

You should first test your sequence of GNUPlot commands on an original build with version number '6.0.0' by selecting svg terminal as the graphical output option and make sure it does not emit warning or error messages. Providing erroneous commands to GDMatPlot may cause SEGFAULT or memory leaks.

Demos

2D demo

It includes several test plots which utilizes a large aspect of GNUPlot of functionality. A test view is shown below.

Demo2D screenshot

3D demo

In this demo, similar test plots are embedded into 3D game world by using subviewports.

Demo3D screenshot

demo3d_screencast.mp4

Classes

GDMatPlotNative

Base class of GDMatPlot derived from Node2D.

Methods

  • draw_plot(): Overrides Node2D _draw method. See the example below for its usage.
  • load_gnuplot(p_path: String = "user://libgnuplot.so"): Loads GNUPlot shared library by using p_path as a temporary loading location. This function must be called initially, preferably in _ready() method. It returns 0 on success, a negative value on error. Note that, if there is already a file at p_path, it will be overwritten.
⚠️ WARNING This function creates 2MB sized temporary file when called. It is better not to call it frequently.
  • run_command(p_cmd: String): Redirects p_cmd to GNUPlot's command line parser.
  • set_dataframe(p_data: PackedFloat64Array, p_column_count: int): Set dataframe to be parsed and plotted. p_data is assumed to be in row major format and its size must be divisible by p_column_count. Imitates GNUPlot's numeric dataframe loading from a text file.
var df: PackedFloat64Array = PackedFloat64Array([ 1, 2, 3, 4, 5, 6, 7... ])
set_dataframe(df, 3)
# This will be interpreted by GNUPlot as
# Column index: 1   2   3
# -------------------------
#               1   2   3
#               4   5   6
#               7   .   .
# Check gnuplot documentation and examples how to plot dataframe columns
  • load_dataframe(): Reset dataframe parser's internal state. It should be called at the beginning of each draw call.
  • start_renderer(p_loop_func: Callable): GNUplot command parser and renderer should be run in a separate thread beacuse it introduces an easily recognizable performance decrease. You should call run_command in the renderer loop function p_loop_func.
  • stop_renderer(): Stops renderer thread.
  • set_rendering_period(p_loop_period: int): Set the time rate of calling p_loop_func defined by start_renderer. The unit of p_loop_period is milliseconds.

Properties

  • transparency: float: Sets plot's background transparency.
  • antialiasing: bool: Enable/disable antialiased line drawing.

Usage

Since version 0.2.0 GDMatPlot imposes a specific programming pattern because GNUPlot's command parsing and rendering add an overhead which does not fit the performace requirements of a game engine. Instead of directly drawing the results of run_command in _draw function, run_command it is called in a renderer loop which is called in a separate thread. In the renderer thread GNUPlot processes commands and generates draw line, polygon, point etc. calls which are encoded in an efficient data structure. Then, a new draw call is queued by calling queue_redraw as shown in the example below. When new drawing triggered, encoded draw calls are decoded and converted to CanvasItems draw_polyline, draw_polygon, etc. calls. This method cancels the negative effect of GNUPlot's command parsing on the frame rate (FPS).

extends GDMatPlotNative

var dataframe: PackedFloat64Array
var lib_loaded: bool = false
var figure_size: Vector2i = Vector2i(640, 480)
var renderer_period: int = 100
var update_df_period: float = 0.1
var col_count: int = 4
var sample_count: int = 100
# Protect access to dataframe from GNUPlot renderer thread
var df_lock: Mutex = Mutex.new()
func _ready():
	transparency = 0.75 # Set plot's transparency
	antialiasing = true # Enable antialized lines
	dataframe.resize(sample_count)
	for i in range(0, sample_count, col_count):
		var x: float = i - sample_count / 2.0
		dataframe[i+0] = x # 1st column of dataframe
		dataframe[i+1] = x / pow(sample_count / 2.0, 1) # 2nd column
		dataframe[i+2] = x * x / pow(sample_count / 2.0, 2) # 3rd column
		dataframe[i+3] = x * x * x / pow(sample_count / 2.0, 3) # 4th column

	var error: int = load_gnuplot()
	if error == 0:
		lib_loaded = true
		set_dataframe(dataframe, col_count) # 4 columns 100/4 = 25 rows
		start_renderer(_draw_commands)
		set_rendering_period(renderer_period)

func _draw_commands():
	if lib_loaded:
		df_lock.lock()
		load_dataframe()
		df_lock.unlock()
		"""GNUPlot commands"""
		run_command("set terminal gdmp size %d,%d" % [figure_size.x, figure_size.y])
		run_command("set grid on")
		# 'df' is a placeholder, it can be any alphanumeric string.
		run_command("plot 'df' using 1:2 with lines, 'dfasd' using 1:3 with lines, 'df' using 1:4 with lines")

func update_dataframe():
	# ... process dataframe ...#
	for i in range(sample_count):
		dataframe[i] *= 0.99
	if lib_loaded:
		df_lock.lock()
		set_dataframe(dataframe, col_count)
		df_lock.unlock()

"""
You don't need to define _draw function. GDMatPlotNative overrides it
and draw_plot will be autmatically called. You should define _draw here
if you want to draw some other stuff (e.g. an image) before and after draw_plot.
"""
func _draw():
	# draw some stuff behind the plot
	draw_circle(Vector2(), 250, Color.AQUA)
	if lib_loaded:
		draw_plot()
	# draw other stuff over the plot
	draw_circle(figure_size, 250, Color(0.0, 0.2, 0.0, 0.5))

var some_condition: bool = false
var total_time: float = 0.0
"""
You should call `queue_redraw` manually after an event or at a desired period.
Otherwise, plot will not be redrawn.
"""
func _process(delta):
	if total_time > update_df_period:
		update_dataframe()
		queue_redraw()
		total_time = 0
	total_time += delta
	if lib_loaded and some_condition:
		# Pause renderer when ever you want by setting a very large period.
		set_rendering_period(1000000000)

Output of the script:

Example plot

Build from source

Requirements

Currently, it can be built on a Linux host. Building the Windows release requires MinGW 12.2.0 or newer.

git clone https://github.com/dmrokan/gdmatplot.git
cd gdmatplot
git submodule update --init --recursive
scons -f SConscript platform='<linux|windows>'
cd godot_cpp
scons target='<template_debug|template_release>' platform='<linux|windows>' arch='x86_64'
cd ..
scons target='<template_debug|template_release>' platform='<linux|windows>' arch='x86_64'

You can visit Godot's build system documentation for more information.

About

This plugin adds GNUPlot terminal to the Godot engine for displaying plots on a 2D canvas layer without requiring the GNUPlot executable

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published