SusuJava (FLOSS Java sim) now uses Khronos' GLES2 (includes JNI for SDL2).
[Version of post is SusuJava@d0ab352.]
Discussion
SusuJava is a simple group of java classes (produced to encourage more java uses), but which does include some simple demos (FishSim.java shows how to use the included java classes to produce a simple artificial ocean with a thousand small fish which swim around).
The copilot/replace-javafx-with-sdl2-opengles branch includes:
The other files of source code are generic java classes (those do not use Khronos tools such as GLES2, but are also published FLOSS (usable through GPLv2 or Apache 2)).
Current plans: merge the copilot/replace-javafx-with-sdl2-opengles branch into the new branch plus into the pos2 branch, as soon as susuwu/sdl_gles2_jni.c is removed (the goal of SusuJava is to use pure java.)
Problems: no regressions on Ubuntu (from the switch from javafx to GLES2), but still can not execute on Termux due to:
~/SusuJava $ ./build.sh
MESA: error: ZINK: failed to choose pdev
libEGL warning: egl: failed to create dri2 screenIf you wish this to continue (to have more new versions published), then give suggestions for how Termux should execute the new GLES2 version of SusuJava.
Previous post (which includes javafx version of source code):
GLES2 version of source code
/* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com)
* <https://github.com/SwuduSusuwu/SusuJava/> has the newest version of `./susuwu/sdl_gles2_jni.c` (henceforth "*this source code*").
* If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use).
* If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those.
*/
/* JNI native implementation for `susuwu.SdlGles2`. Build: see `build.sh`. */
/* Usage: compile as shared library, then `System.loadLibrary("sdl_gles2_jni")` loads this. */
#include <jni.h> /* JNI types + macros */
#include <SDL2/SDL.h> /* SDL_Init, SDL_CreateWindow, SDL_GL_CreateContext, SDL_PollEvent, SDL_GL_SwapWindow, SDL_Quit */
#include <GLES2/gl2.h> /* glClear, glClearColor, glCreateShader, glCreateProgram, glDrawArrays, ... */
#include <stdio.h> /* fprintf, stderr */
#include <string.h> /* NULL */
static SDL_Window *g_window = NULL;
static SDL_GLContext g_context = NULL;
static GLuint g_program = 0;
static GLint g_posAttrib = -1;
static GLint g_colorUniform = -1;
static GLint g_resolutionUniform = -1;
static int g_width = 0, g_height = 0;
/* Minimal vertex shader: converts pixel coords to clip space, flips Y so (0,0) is top-left (matches JavaFX canvas). */
static const char *VERT_SRC =
"attribute vec2 a_position;\n"
"uniform vec2 u_resolution;\n"
"void main() {\n"
" vec2 zeroToOne = a_position / u_resolution;\n"
" vec2 clipSpace = zeroToOne * 2.0 - 1.0;\n"
" gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);\n"
"}\n";
/* Minimal fragment shader: outputs a uniform solid color. */
static const char *FRAG_SRC =
"precision mediump float;\n"
"uniform vec4 u_color;\n"
"void main() {\n"
" gl_FragColor = u_color;\n"
"}\n";
static GLuint compile_shader(GLenum type, const char *src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if(!compiled) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
fprintf(stderr, "sdl_gles2_jni: shader compile error: %s\n", log);
glDeleteShader(shader);
return 0;
}
return shader;
}
/* Java_susuwu_SdlGles2_init: creates SDL2 window + GLES2 context + compiles shaders. */
JNIEXPORT jboolean JNICALL Java_susuwu_SdlGles2_init(JNIEnv *env, jclass cls, jint width, jint height, jstring jtitle) {
if(SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "sdl_gles2_jni: SDL_Init failed: %s\n", SDL_GetError());
return JNI_FALSE;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
const char *title = (*env)->GetStringUTFChars(env, jtitle, NULL);
g_window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
(int)width, (int)height, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
(*env)->ReleaseStringUTFChars(env, jtitle, title);
if(!g_window) {
fprintf(stderr, "sdl_gles2_jni: SDL_CreateWindow failed: %s\n", SDL_GetError());
SDL_Quit();
return JNI_FALSE;
}
g_context = SDL_GL_CreateContext(g_window);
if(!g_context) {
fprintf(stderr, "sdl_gles2_jni: SDL_GL_CreateContext failed: %s\n", SDL_GetError());
SDL_DestroyWindow(g_window);
g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_width = (int)width;
g_height = (int)height;
GLuint vert = compile_shader(GL_VERTEX_SHADER, VERT_SRC);
GLuint frag = compile_shader(GL_FRAGMENT_SHADER, FRAG_SRC);
if(!vert || !frag) {
if(vert) glDeleteShader(vert);
if(frag) glDeleteShader(frag);
SDL_GL_DeleteContext(g_context); g_context = NULL;
SDL_DestroyWindow(g_window); g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_program = glCreateProgram();
glAttachShader(g_program, vert);
glAttachShader(g_program, frag);
glLinkProgram(g_program);
glDeleteShader(vert);
glDeleteShader(frag);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
if(!linked) {
char log[512];
glGetProgramInfoLog(g_program, sizeof(log), NULL, log);
fprintf(stderr, "sdl_gles2_jni: program link error: %s\n", log);
glDeleteProgram(g_program); g_program = 0;
SDL_GL_DeleteContext(g_context); g_context = NULL;
SDL_DestroyWindow(g_window); g_window = NULL;
SDL_Quit();
return JNI_FALSE;
}
g_posAttrib = glGetAttribLocation(g_program, "a_position");
g_colorUniform = glGetUniformLocation(g_program, "u_color");
g_resolutionUniform = glGetUniformLocation(g_program, "u_resolution");
glUseProgram(g_program);
glUniform2f(g_resolutionUniform, (float)g_width, (float)g_height);
glViewport(0, 0, g_width, g_height);
return JNI_TRUE;
}
/* Java_susuwu_SdlGles2_destroy: tears down GLES2 + SDL2. */
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_destroy(JNIEnv *env, jclass cls) {
if(g_program) { glDeleteProgram(g_program); g_program = 0; }
if(g_context) { SDL_GL_DeleteContext(g_context); g_context = NULL; }
if(g_window) { SDL_DestroyWindow(g_window); g_window = NULL; }
SDL_Quit();
}
/* Java_susuwu_SdlGles2_pollQuit: drains SDL event queue; returns JNI_TRUE if app should exit. */
JNIEXPORT jboolean JNICALL Java_susuwu_SdlGles2_pollQuit(JNIEnv *env, jclass cls) {
SDL_Event event;
while(SDL_PollEvent(&event)) {
if(SDL_QUIT == event.type) {
return JNI_TRUE;
}
if(SDL_KEYDOWN == event.type && SDLK_ESCAPE == event.key.keysym.sym) {
return JNI_TRUE;
}
}
return JNI_FALSE;
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_glClearColor(JNIEnv *env, jclass cls, jfloat r, jfloat g, jfloat b, jfloat a) {
glClearColor(r, g, b, a);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_glClear(JNIEnv *env, jclass cls, jint mask) {
glClear((GLbitfield)mask);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_swapWindow(JNIEnv *env, jclass cls) {
SDL_GL_SwapWindow(g_window);
}
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_setWindowTitle(JNIEnv *env, jclass cls, jstring jtitle) {
if(!g_window) { return; }
const char *title = (*env)->GetStringUTFChars(env, jtitle, NULL);
SDL_SetWindowTitle(g_window, title);
(*env)->ReleaseStringUTFChars(env, jtitle, title);
}
/* Java_susuwu_SdlGles2_drawFilledPolygon: draws pre-triangulated vertices (multiples of 3) in screen coords with solid color. */
JNIEXPORT void JNICALL Java_susuwu_SdlGles2_drawFilledPolygon(JNIEnv *env, jclass cls, jfloatArray jverts, jfloat r, jfloat g, jfloat b, jfloat a) {
jsize len = (*env)->GetArrayLength(env, jverts);
jfloat *verts = (*env)->GetFloatArrayElements(env, jverts, NULL);
glUseProgram(g_program);
glUniform4f(g_colorUniform, r, g, b, a);
glVertexAttribPointer(g_posAttrib, 2, GL_FLOAT, GL_FALSE, 0, verts);
glEnableVertexAttribArray(g_posAttrib);
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(len / 2));
glDisableVertexAttribArray(g_posAttrib);
(*env)->ReleaseFloatArrayElements(env, jverts, verts, JNI_ABORT);
}/* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com)
* <https://github.com/SwuduSusuwu/SusuJava/> has the newest version of `./susuwu/SdlGles2.java` (henceforth "*this source code*").
* If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use).
* If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those.
*/
package susuwu; /* Usage: `import susuwu.SdlGles2;` */
/**
* {@code class SdlGles2} is a thin JNI bridge to SDL2 and OpenGL ES 2.0.
* Replaces JavaFX {@code Stage}, {@code Scene}, {@code Canvas}, {@code GraphicsContext}, {@code AnimationTimer}, {@code Timeline}.
* Usage: {@code SdlGles2.init(width, height, title);} then render loop, then {@code SdlGles2.destroy();}
*/
public class SdlGles2 {
static {
System.loadLibrary("sdl_gles2_jni"); /* Loads `libsdl_gles2_jni.so` (or `.dll`/`.dylib`). Build: see `build.sh`. */
}
public static final int GL_COLOR_BUFFER_BIT = 0x00004000; /* Matches `GL_COLOR_BUFFER_BIT` from `<GLES2/gl2.h>` */
/** Creates the SDL2 window + OpenGL ES 2.0 context. Returns {@code true} on success. Replaces {@code Stage}, {@code Scene}. */
public static native boolean init(int width, int height, String title);
/** Destroys the SDL2 window + context. Calls {@code SDL_Quit()}. Replaces {@code stage.close()}. */
public static native void destroy();
/** Polls SDL events; returns {@code true} if SDL_QUIT or Escape was received (main loop should exit). Replaces {@code AnimationTimer}/{@code Timeline} termination. */
public static native boolean pollQuit();
/** Sets the GLES2 clear color. Replaces {@code Scene} background color. */
public static native void glClearColor(float r, float g, float b, float a);
/** Clears the GLES2 framebuffer. {@code mask} should be {@link #GL_COLOR_BUFFER_BIT}. Replaces {@code gc.clearRect()}. */
public static native void glClear(int mask);
/** Swaps front/back buffers (presents the rendered frame). Replaces implicit JavaFX frame commit. */
public static native void swapWindow();
/** Sets the window title. Used for FPS text display. Replaces {@code javafx.scene.text.Text}. */
public static native void setWindowTitle(String title);
/**
* Draws a filled polygon as triangles in screen space.
* {@code vertices}: interleaved {@code [x0,y0, x1,y1, ...]} in pixels (already transformed to screen coords).
* Vertex count must be a multiple of 3 (pre-triangulated input). Replaces {@code gc.beginPath/moveTo/lineTo/fill}.
*/
public static native void drawFilledPolygon(float[] vertices, float r, float g, float b, float a);
}#!/bin/sh
#
# /* This is the new build script for `./susuwu/FishSim.java`. */
# /* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com)
# * <https://github.com/SwuduSusuwu/SusuJava/> has the newest version of `./build.sh` (henceforth "*this source code*").
# * If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use).
# * If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those. */
PATH_TO_CLASS="susuwu/FishSim"
PATH_TO_SOURCE="${PATH_TO_CLASS}.java"
PATH_TO_NATIVE="susuwu/sdl_gles2_jni.c"
PATH_TO_NATIVE_LIB="susuwu/libsdl_gles2_jni.so" # /* `.so` on Linux/Android, `.dll` on Windows, `.dylib` on macOS */
JAVA_FLAGS="${JAVA_FLAGS} -enableassertions" # /* Notice: remove `-enableassertions` so performance improves */
JAVA_FLAGS="${JAVA_FLAGS} -Djava.library.path=susuwu" # /* Allows JNI to find `libsdl_gles2_jni.so` */
export JAVA_BUILD_TEST_FLAGS="-verbose"
export JAVA_TEST_FLAGS="-verbose:module"
if command -v sudo >/dev/null; then
APTITUDE="sudo apt -y install "
else
APTITUDE="apt -y install " # /* Fixes "The program sudo is not installed." on platforms such as smartphones */
fi
command -v java >/dev/null || ${APTITUDE} openjdk-21-jdk-headless || ${APTITUDE} default-jdk-headless
if ! dpkg -l libsdl2-dev >/dev/null 2>&1; then # /* Install SDL2 + GLES2 dev headers (replaces `openjfx`) */
${APTITUDE} libsdl2-dev libgles2-mesa-dev || true
fi
# /* Compile the JNI native library: `libsdl_gles2_jni.so` (replaces `--module-path`/`--add-modules javafx.*`) */
JAVA_HOME="${JAVA_HOME:-$(java -XshowSettings:properties -version 2>&1 | grep 'java.home' | sed 's/.*= //')}"
JNI_INCLUDES="-I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux" # /* Linux; macOS uses `include/darwin`, Android uses NDK paths */
#shellcheck disable=SC2086 # /* Quotes cause errors with pkg-config output */
cc -shared -fPIC "${PATH_TO_NATIVE}" -o "${PATH_TO_NATIVE_LIB}" ${JNI_INCLUDES} $(pkg-config --cflags --libs sdl2) -lGLESv2 || exit $?
if [ -n "${GITHUB_ACTIONS}" ]; then
#shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */
javac ${JAVA_BUILD_TEST_FLAGS} susuwu/SdlGles2.java susuwu/SimUsages.java ${PATH_TO_SOURCE} # /* Gives "Missing JavaFX application class susuwu/FishSim" unless `cd $(dirname ${PATH_TO_CLASS})` is used. */
else
#shellcheck disable=SC2086 # /* Quotes cause "Unrecognized option:" */
java ${JAVA_FLAGS} --source 16 ${PATH_TO_SOURCE} # /* `--source` is workaround for "error: cannot find symbol\n...\n symbol: {class Force, variable Utils}" when not compiling all sources together */
fi
exit $? #Status required so [*CodeQL*](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) passes./* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com)
* <https://github.com/SwuduSusuwu/SusuJava/> has the newest version of `./susuwu/SimUsages.java` (henceforth "*this source code*").
* If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use).
* If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those.
*/
package susuwu; /* Usage: `import susuwu.SimUsages;` */
/**
* {@code class SimUsages} shows {@code FpsTextMode} statistics such as {@code fps} or {@code ms}.
* Requirements: some render loop (for measurements). Is not specific to the renderer used.
* Was produced for {@code susuwu.FishSim}, so the text (plus comments) assume the organisms are {@code class Fish}, but {@code SimUsages} is not specific to {@code class Fish}
* Some {@code assert}s follow, thus document which arguments to use with this (without {@code -enableassertions}, thus are not enforced).
* Some "Usage:" comments follow, which document how to use this.
* Usage: {@code SimUsages usages = new SimUsages(); usages.show(); usages.fpsTextMode = FpsTextMode.fps.value | FpsTextMode.ms.value;}
* Text is rendered via {@link SdlGles2#setWindowTitle} (replaces {@code javafx.scene.text.Text}).
*/
public class SimUsages {
/* `public` members */
public long fpsTextMode = FpsTextMode.allUsages.value; // Usage `usages.fpsTextModeFps = FpsTextMode.fps.value;` to just show `fps`
public double secondsPerFpsTextRefresh = 1.0; // Usage: `usages.secondsPerFpsTextRefresh = 0.2;` to give more current values, or `= 2.0;` to give more smooth values. In `postRefresh()`: if `secondsPerFpsTextRefresh` elapses, `lastTime = System.nanoTime(); fpsTextRefresh();``
public double renderMs = Double.NaN; // Usage: `functionWhichUsesRenderMs(usages.renderMs);`. Stores average **ms** from `startRender()` to `postRender()` (`renderNs / renderCounter / 1_000_000.0`)
public double physicsMs = Double.NaN; // Usage: `functionWhichUsesPhysicsMs(usages.physicsMs);`. Stores average **ms** from `startPhysics()` to `postPhysics()` (`physicsNs / physicsCounter / 1_000_000.0`)
public double fps = Double.NaN; // Usage: `functionWhichUsesFps(usages.fps);`. Stores `renderCounter / (System.nanoTime() - lastTime) / 1_000_000_000.0`
/* `private` or almost-`private` members */
public long lastTime = System.nanoTime(); // Stores `System.nanoTime()` when `renderCounter = 0`.
public int refreshCounter = 0; // Sum of `postRefresh()` uses since `SimUsages(Pane)`.
public int renderCounter = 0; // Sum of `postRender()` uses since `lastTime = System.nanoTime()`.
public int physicsCounter = 0; // Sum of `postPhysics()` uses since `lastTime = System.nanoTime()`.
public long physicsNs = -1; // Sum of `nanoTime()` at `postRender()` minus `nanoTime()` at `startRender()` since `lastTime = System.nanoTime()`.
public long renderNs = -1; // Sum of `nanoTime()` at `postRender()` minus `nanoTime()` at `startRender()` since `lastTime = System.nanoTime()`.
private String fpsText = "0 FPS";
public SimUsages() {
/* No Pane or display object needed: text is shown via SdlGles2.setWindowTitle(). */
}
static public enum FpsTextMode { // `FpsTextMode` says which resources `fpsText` will show.
none (0 ), // `fpsText = "";`
fps (1 << 0), // `fpsText` += `fps` "FPS";
ms (2 << 1), // `fpsText` += `ms` "ms"; /* Notice: `ms = 1000 / fps;`, so includes idle CPU */
msSpec (1 << 2), // `fpsText` += `renderMs` "renderMs," `physicsMs` "physicsMs"; /* Notice: uses `System.nanoTime()`, does not include idle CPU */
msFish (1 << 3), // `fpsText` += `renderMs / fishShown` "renderMs / Fish shown," `physicsMs / fishListSize` "physicsMs / Fish";
fish (1 << 4), // `fpsText` += `fishListSize` "Fish";
fishShown (1 << 5), // `fpsText` += `fishShown` "Fish shown";
allUsages (FpsTextMode.fps.value | FpsTextMode.ms.value | FpsTextMode.msSpec.value | FpsTextMode.msFish.value | FpsTextMode.fish.value | FpsTextMode.fishShown.value); // shows as all supported usages
long value; // Stores bitwise-or of those.
FpsTextMode(long value) { this.value = value; }
}; // TODO: replace manual bitshifts with `java.util.EnumSet<E>`?
public void show() {
SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setX/Y/setFill(Color.WHITE)`: text is in window title. */
}
/* Measurement funtions
* Usage: ```
* refreshLoop() {
* usages.startRefresh();
* usages.startPhysics(); physics(fishList); usages.postPhysics();
* usages.startRender(); render(visibleFish); usages.postRender();
* usages.postRefresh(System.nanoTime(), visibleFish.size(), fishList.size());
* }```
* "Refresh loop" is the loop which invokes the render loop plus the physics loop. `class SimUsages` does not assume that the physics loop is invoked as often as the render loop is. If the renderer has its own separate loop, the render loop is the refresh loop.
*/
public void startRefresh() { // Usage: `startRender();` at start of refresh loop.
}
public void postRefresh(long now, long fishShown, long fishListSize) { // Usage: `postRefresh(System.nanoTime(), visibleObjects.size(), physisObjects.size());`
double elapsed = (now - lastTime) / 1_000_000_000.0;
if(elapsed >= secondsPerFpsTextRefresh) {
lastTime = now;
fps = renderCounter / elapsed;
renderMs = renderNs / renderCounter / 1_000_000.0;
physicsMs = physicsNs / physicsCounter / 1_000_000.0;
fpsTextRefresh(fishShown, fishListSize); /* Replaces `Platform.runLater(...)`: SDL2 has no UI thread restriction, so call directly. */
renderCounter = 1;
renderNs = -1;
physicsCounter = 1;
physicsNs = -1;
}
refreshCounter++;
}
private long renderNsStart, physicsNsStart;
public void startRender() { // Usage: `startRender();` at start of render loop
renderNsStart = System.nanoTime();
}
public void preSynchro() { // Usage: `startRender(); ... startSynchro(); SdlGles2.glClear(...); postSynchro();`
renderNsStart += System.nanoTime();
} // TODO: reduce `preSynchro()` to no-op, improve `postSynchro()` to subtract actual Virtual Synchronization time from `renderNsStat` (so the time to clear buffers is included).
public void postSynchro() { // Usage: `startRender(); ... startSynchro(); SdlGles2.glClear(...); postSynchro();`
renderNsStart -= System.nanoTime(); // Purpose: subtracts vertical synchronization time from "drawMS` (which is computed from `renderNs`, which is computed from `renderNsStart`). Problem: `glClear()` does not just wait for Virtical Synchronization, but also clears the buffer, which this will also subtract from `renderNs`.
}
public void postRender() { // Usage: `startRender();` at closure of render loop
renderNs += System.nanoTime() - renderNsStart;
renderCounter++;
}
public void startPhysics() { // Usage: `startPhysics();` at start of physics loop
physicsNsStart = System.nanoTime();
}
public void postPhysics() { // Usage: `startPhysics();` at closure of physics loop
physicsNs += System.nanoTime() - physicsNsStart;
physicsCounter++;
}
private void fpsTextRefresh(long fishShown, long fishListSize) { /* Usage: `usages.fpsTextModeFps(visibleFish.size(), fishList.size())` */
boolean fpsTextModeFps = (0 != (FpsTextMode.fps.value & fpsTextMode));
boolean fpsTextModeMs = (0 != (FpsTextMode.ms.value & fpsTextMode));
boolean fpsTextModeMsSpec = (0 != (FpsTextMode.msSpec.value & fpsTextMode));
boolean fpsTextMsSpecFish = (0 != ((FpsTextMode.msSpec.value | FpsTextMode.msFish.value) & fpsTextMode)); // TODO: replace manual bitshifts with `java.util.EnumSet<E>`?
boolean fpsTextModeMsFish = (0 != (FpsTextMode.msFish.value & fpsTextMode));
boolean fpsTextModeFish = (0 != (FpsTextMode.fish.value & fpsTextMode));
boolean fpsTextModeFishShown = (0 != (FpsTextMode.fishShown.value & fpsTextMode));
double totalMs = 1 / fps * 1000;
String fpsTextStr = "";
String strSep = ", ", strJoin = " (";
if(FpsTextMode.none.value == fpsTextMode) { return; }
if(fpsTextModeFps) {
fpsTextStr += String.format("%4.2f FPS" + strSep, fps);
}
if(fpsTextModeMs) {
fpsTextStr += String.format("%4.2f MS" + (fpsTextMsSpecFish ? strJoin : strSep), totalMs);
}
if(fpsTextModeMsSpec) {
fpsTextStr += String.format("%4.2f drawMS, %4.2f physicsMS", renderMs, physicsMs);
fpsTextStr += (fpsTextModeMsFish ? strSep : "");
}
if(fpsTextModeMsFish) {
fpsTextStr += String.format("%2.4f drawMS / Fish shown, %2.4f physicsMS / Fish", renderMs / fishShown, physicsMs / fishListSize);
}
if(fpsTextMsSpecFish) {
if(0 != ((FpsTextMode.ms.value & fpsTextMode))) {
fpsTextStr += ")";
}
fpsTextStr += strSep;
}
if(fpsTextModeFish) {
fpsTextStr += String.format("%4d Fish", fishListSize);
fpsTextStr += (fpsTextModeFishShown ? strJoin : strSep);
}
if(fpsTextModeFishShown) {
fpsTextStr += String.format(fpsTextModeFish ? "%4d shown)" : "%4d Fish shown", fishShown);
fpsTextStr += strSep;
}
fpsText = fpsTextStr.substring(0, fpsTextStr.length() - strSep.length());
SdlGles2.setWindowTitle("Fish Simulation (Boids) - " + fpsText); /* Replaces `Text.setText(...)`: update window title with FPS stats. */
}
};/* Attribution (henceforth "*this attribution*", whose syntax is *Markdown*): 2024 [Swudu Susuwu](https://swudususuwu.substack.com)
* <https://github.com/SwuduSusuwu/SusuJava/> has the newest version of `./susuwu/FishSim.java` (henceforth "*this source code*").
* If *this attribution* is shown, *this source code* allows all uses. *This attribution* constitutes the most permissive which is compatible with [*GPLv2*](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + [*Apache 2*](https://www.apache.org/licenses/LICENSE-2.0.html), which is suitable for personal use (also suitable for school use).
* If *this attribution* is not professional enough for business use: businesses can use *this source code* through included versions of [*GPLv2*](./LICENSE_GPLv2), [*Apache 2*](./LICENSE), or through both of those.
*/
package susuwu; /* Usage: `import susuwu.FishSim;` */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import susuwu.SimUsages; /* `class SimUsages`, `enum FpsTextMode` */
import susuwu.SdlGles2; /* `class SdlGles2`: JNI bridge to SDL2 + GLES2 */
import susuwu.Calculus; /* `Calculus.pow2()` */
import susuwu.Forces; /* `class Forces implements java.lang.Cloneable` */
import susuwu.ImmutablePosBounds; /* `enum PosBoundsMode`: which stores how sims enforce bounds. */
import susuwu.PosBounds; /* `class PosBounds : extends ImmutablePosBounds`, `PosBounds.set*(PosBounds*)` */
import susuwu.ImmutablePos; /* `class ImmutablePos implements java.util.RandomAccess` */
import susuwu.Pos; /* `class Pos extends ImmutablePos` */
import susuwu.Pos2; /* `class Pos extends Pos` */
// public static class Pos2 extends double[2] {} // `{Pos2[0], Pos2[1]}` is `{x, y}` position (or resolution), or is `{pos[0], pos[0]}` motion (derivative of position), or is is `{d2x, d2y}` acceleration (derivative number 2). This was supposed to do what `typedef` does (wish for future-proof (limitless dimensions) virtual `class` with functions for numerous transforms).
// TODO: test how much of `java`'s [static `Array` overhead](https://github.com/SwuduSusuwu/SusuPosts/blob/preview/posts/Physics_sims_which_structures_to_use.md#separate-variables-versus-dim-lists) `java`'s toolkit optimizes for you. If performance is a problem, choose a new approach to use.
/**
* Simple SDL2+GLES2 fish sim (via JNI), which includes reusable {@code public class}s (for new sims to use). Most of the reusable {@code public class}s are in other {@code .java} sources for {@code package susuwu}
* This ([`./susuwu/FishSim.java`](./FishSim.java)) uses pseudo-*Markdown* for comments, but [`./posts/FishSim.md`](../posts/FishSim.md) is the actual [*Markdown*](https://github.github.com/gfm/) document for this.
* Notice: replaced most of [*Solar-Pro-2*'s original `FishSim.java`](https://github.com/SwuduSusuwu/SusuJava/blob/solarPro2FishSim/susuwu/FishSim.java), as [`./posts/FishSim.md#intro`](../posts/FishSim.md#intro) documents (plus [*GitHub*'s `/compare/` tool shows](https://github.com/SwuduSusuwu/SusuJava/compare/solarPro2FishSim..susuFishSim#diff-8c440bb92bc6939e1450542897e0bbb1a8737b93808ea63ed32784edfacef4b4).
*/
public class FishSim {
/** Minimal {@code Color} replacement (replaces {@code javafx.scene.paint.Color}). Supports {@code getRed()}, {@code getGreen()}, {@code getBlue()} for {@code isSimilarTo()} comparisons. */
public static class Color {
private final double red, green, blue;
private Color(double r, double g, double b) { this.red = r; this.green = g; this.blue = b; }
public static Color color(double r, double g, double b) { return new Color(r, g, b); }
public double getRed() { return red; }
public double getGreen() { return green; }
public double getBlue() { return blue; }
public static final Color LIGHTBLUE = new Color(0.678, 0.847, 0.902); /* Light blue background (replaces `Color.LIGHTBLUE` from JavaFX) */
}
public enum PhysicsMode { // `PhysicsMode` says how to execute `updateFish()`
synchronousHomo, // `updateFish()` once per `refreshLoop()`.
synchronousInterval, // `updateFish()` per `positionInterval` `refreshLoop()`s.
asynchronousHomo, // `executor.submit(() -> updateFish());` once per `refreshLoop()`.
asynchronousInterval, // `executor.submit(() -> updateFish());` per `positionInterval` `refreshLoop()`s.
separateUnbound, // `updateFish()` runs in a background thread continuously (replaces `AnimationTimer`). Notice: with `GLES2` this has an implicit bound to the monitor refresh (Virtual Synchronization, which `SimUsages` does not count towards "drawMS").
separateFps, // `updateFish()` runs via `ScheduledExecutorService` at `physicsRefreshHertz` (replaces `Timeline/KeyFrame`).
}
private static PhysicsMode monitorRefreshMode = PhysicsMode.separateFps; // `monitorRefreshMode` must use `.separateUnbound` or `.separateFps`.
private static PhysicsMode physicsMode = PhysicsMode.separateFps; // Notice: if `PhysicsMode.*Interval`, must set `positionInterval`. if `PhysicsMode.separateFps`, must set `physicsRefreshHertz`.
static ReentrantLock renderFishLock = new ReentrantLock();
static ReentrantLock updateFishLock = new ReentrantLock();
// TODO: remove `static` from {`resolution`, `bounds`}, to allow to remove `static` from {`setResolution()`, `renderFishLock, `updateFishLock`}, so `FishSim` allows numerous windows
public static boolean setResolution(int[] newResolution) { /* Notice: invalidates references to old `bounds`, `*Slash2` addresses. */ // TODO: remove `static` from {`resolution`, `bounds`}, to allow to remove `static` from {`setResolution()`, `renderFishLock, `updateFishLock`}, so `FishSim` allows numerous windows
assert 2 == newResolution.length;
assert 0 < newResolution[0]; //TODO: allow "headless" instances with `resolution = {0, 0}`?
assert 0 < newResolution[1];
updateFishLock.lock();
renderFishLock.lock();
resolution = newResolution;
resolutionf.pos[0] = resolution[0]; resolutionf.pos[1] = resolution[1];
resolutionfSlash2 = resolutionf.slashScalar(2); /* Notice: invalidates references which store the old address to `resolutionfSlash2`. */
resVolume = (int)Math.round(resolutionf.volume()); /* Notice: uses `Pos::volume()` since simple source code is less bug prone. `Math.round` ensures 24-bit mantissas give accurate values */
posBounds.setBounds(resolutionf.starScalar(boundsResolutionFactor).pos); // TODO: if sure that no functions store references to the original instance, replace the above row with this (since simple source code is less bug prone)
// (Resize SDL window here if needed: SDL_SetWindowSize(window, resolution[0], resolution[1]))
// (Resize GLES2 viewport here if needed: glViewport(0, 0, resolution[0], resolution[1]))
// (Rebuild GLES2 u_resolution uniform: SdlGles2.glClearColor/etc. if resolution changes)
renderFishLock.unlock();
updateFishLock.unlock();
return true;
}
private static int[] resolution = {1280, 720};
private static Pos2 resolutionf = new Pos2(resolution[0], resolution[1]);
private static Pos resolutionfSlash2 = resolutionf.slashScalar(2); /* Improves execution of inner loops which use this. Notice: `setResolution(newResolution)` invalidates stored references to `resolutionfSlash2` */
private static int resVolume = (int)Math.round(resolutionf.volume()); /* Notice: uses `Pos::volume()` since simple source code is less bug prone. `Math.round` ensures 24-bit mantissas give accurate values */
private static double boundsResolutionFactor = (1_000_000 > resVolume ? 2.0 : 1.2); // Ocean is `resolution[dim] * boundsResolutionFactor`. `2.0` gives more room for natural oceans, but old laptops with huge resolutions (such as `{2200, 1200}`) must use `1.2` so the load is low enough for old CPUs to process). TODO: include short benchmark (on startup) to set `boundsResolutionFactor` to optimal value, or reduce CPU use for unshown `Fish` (`if(!fish.isVisible)`, then execute `updateFish` just once per second (1 hertz), with larger steps).
private static PosBounds posBounds = new PosBounds(PosBounds.PosBoundsMode.wrapAroundResolution, resolutionf.starScalar(boundsResolutionFactor)); // for simple sims, use `resolutionf.clone()`
private static double fishVolume = 200; // Uses resolution of `Fish::render()`.
private static double fishLengthsSep = 62; // Average `Fish`-lengths distance from `Fish` to `Fish`.
private static double fishPerVolume = 1 / fishVolume / fishLengthsSep; // `Fish` per volume (for 2D, volume is resolution).
private static int fishCount = (int)(posBounds.getBoundsVolume() * fishPerVolume);
private static int gridResolution = 100; // Notice: set this to `Colllections.max({forces*.distance})` (which should equal what most sims call "view distance"), so that all relevent `Fish` are processed.
private static int positionInterval = 2; // The `refreshCounter` per `Fish::applyFlockingRulesUpdate()`
public static double monitorRefreshHertz = 60.0; // The `SimUsages.fps` to wish for // Notice: since this limits `SimUsages.fps` to `monitorRefreshHertz`, this prevents benchmarks which use `FpsTextMode.fps` (or `FpsTextMode.ms`). Benchmarks can still use `FpsTextMode.msSpec` (or `FpsTextMode.msFish`).
public static double physicsRefreshHertz = monitorRefreshHertz / positionInterval; // The `1 / SimUsages.physicsMs` to wish for // Notice: unknown what `javafx.animation.Timeline` does if `physicsRefreshHertz > (1 / SimUsages.physicsMs)`, but guess thus stalls or consumes multiple executors
private List<Fish> fishList = new ArrayList<>();
private int fishShown = 0;
private List<Fish>[][] grid; /* `listToPartitions(List<>[][] grid, List<> list)` uses this */
private Random random = new Random();
SimUsages simUsages = new SimUsages(); /* Replaces `new SimUsages(root)`: no Pane needed for SDL2 text (shown via window title). */
// simUsages.fpsTextMode = FpsTextMode.allUsages.value; // TODO: "error: <identifier> expected" solution
private volatile boolean quit = false; /* Set to `true` by `stop()` to signal the main SDL loop to exit. */
private ExecutorService executor = Executors.newSingleThreadExecutor();
private ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); /* Replaces `javafx.animation.Timeline` for `separateFps` physics. */
public static void main(String[] args) {
new FishSim().run(args); /* Replaces `launch(args)`: instantiate directly since there is no JavaFX Application lifecycle. */
}
/** Initializes SDL2+GLES2, populates fish, starts physics loops, then runs the render loop until quit. Replaces {@code start(Stage primaryStage)}. */
public void run(String[] args) { /* `args` preserved for future CLI configuration (e.g., `--resolution`, `--physics-mode`); currently unused. */
if(!SdlGles2.init(resolution[0], resolution[1], "Fish Simulation (Boids)")) {
System.err.println("FishSim.run: SdlGles2.init failed; aborting.");
return;
}
SdlGles2.glClearColor( /* Light-blue background (replaces `Color.LIGHTBLUE` passed to `new Scene(...)`) */
(float)Color.LIGHTBLUE.getRed(), (float)Color.LIGHTBLUE.getGreen(), (float)Color.LIGHTBLUE.getBlue(), 1.0f);
// Initialize fish
for(int i = 0; i < fishCount; i++) {
Pos2 pos = new Pos2(random.nextDouble() * posBounds.getBounds(0), random.nextDouble() * posBounds.getBounds(1));
Pos2 dpos = new Pos2((random.nextDouble() * 2 - 1) * Fish.dposMax, (random.nextDouble() * 2 - 1) * Fish.dposMax);
fishList.add(new Fish(pos, dpos, Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble())));
}
posBounds.setGridResolution(gridResolution);
grid = new ArrayList[posBounds.getGridSize(0)][posBounds.getGridSize(1)]; /* `listToPartitions(List<>[][] grid, List<> list)` uses this */
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++) {
grid[i][j] = new ArrayList<>();
}
}
simUsages.show();
// Start separate physics loop for `separateUnbound` / `separateFps` modes (replaces `AnimationTimer` / `Timeline`):
switch(physicsMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case separateUnbound:
executor.submit(() -> { while(!quit) { updateFish(); } });
break;
case separateFps:
long physicsIntervalMs = Math.max(1L, (long)(1_000.0 / physicsRefreshHertz)); /* Use milliseconds for scheduler precision (avoids nanosecond scheduler overhead). */
scheduledExecutor.scheduleAtFixedRate(() -> updateFish(), 0, physicsIntervalMs, TimeUnit.MILLISECONDS);
break;
default:
break; /* synchronous* / asynchronous* modes: handled inside `refreshLoop()` */
}
// Main SDL render loop (replaces `AnimationTimer` / `Timeline` for `monitorRefreshMode`):
long renderIntervalNs = (long)(1_000_000_000.0 / monitorRefreshHertz);
long lastRenderTime = System.nanoTime();
while(!quit && !SdlGles2.pollQuit()) {
long now = System.nanoTime();
boolean shouldRender;
switch(monitorRefreshMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case separateUnbound: // Notice: uses Vertical Synchronization, which `SimUsages` subtracts from `renderNs` (does not count towards resource usage).
shouldRender = true;
break;
case separateFps: /* fall-through */
default:
shouldRender = ((now - lastRenderTime) >= renderIntervalNs);
break;
}
if(shouldRender) {
refreshLoop(now);
lastRenderTime = now;
} else {
try { Thread.sleep(1); } catch(InterruptedException e) { Thread.currentThread().interrupt(); break; } /* 1ms sleep avoids busy-waiting while still responding within 1 frame at 60fps (~16ms). */
}
}
stop();
}
private void refreshLoop(long now) {
simUsages.startRefresh();
switch(physicsMode) { // `PhysicsMode.` is omitted from all `case`s, to support old `java --source` versions
case synchronousHomo:
updateFish();
break;
case synchronousInterval:
if(simUsages.refreshCounter % positionInterval == 0) {
updateFish();
}
break;
case asynchronousHomo:
executor.submit(() -> updateFish());
break;
case asynchronousInterval:
if(simUsages.refreshCounter % positionInterval == 0) {
executor.submit(() -> updateFish());
}
break;
case separateUnbound:
case separateFps:
break; // no-op for both, since `run()` processes thus
default:
throw new IllegalArgumentException("Unsupported `PhysicsMode physicsMode`: " + physicsMode);
}
renderFish();
simUsages.postRefresh(now, fishShown, fishList.size());
}
private void outOfBounds(String function, Fish fish) {
/* Notice: `outOfBounds()` has numerous sensible actions other than to print to `stderr`: `fish.die()`, `fish.stop()`, `fish.reverse()`, `fish.wrapAround()` */
System.err.println(function + ": " + posBounds.posOutOfBoundsStr(fish.pos, "Fish.pos"));
}
/* Spatial partitioning (simple grid system). TODO: generic version of this (accept all `class`s with `#isInBounds` plus `#pos`). */
private void listToPartitions(List<Fish>[][] grid, List<Fish> list) {
assert grid.length == (int)Math.ceil(posBounds.getBounds(0) / gridResolution);
assert grid[0].length == (int)Math.ceil(posBounds.getBounds(1) / gridResolution);
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++) {
grid[i][j].clear();
}
}
for(Fish fish : list) { /* Assign list members to grid sections */
if(fish.isInBounds) {
int[] gridPos = {(int) (fish.pos.pos[0] / gridResolution), (int) (fish.pos.pos[1] / gridResolution)};
grid[gridPos[0]][gridPos[1]].add(fish); // if `gridPos` is not in bounds, this will `throw new IndexOutOfBoundsException()`. But `Fish.setPos()` uses `PosBounds::posBounds.posBound()` which, which ensures `Fish.pos` bounds to `FishSim.resolution`, so this will not `throw`.
}
}
}
private void updateFish() {
updateFishLock.lock();
simUsages.startPhysics();
listToPartitions(grid, fishList);
// Update each fish
for(Fish fish : fishList) {
fish.applyFlockingRules(fishList, grid);
fish.update();
}
simUsages.postPhysics();
updateFishLock.unlock();
}
private void renderFish() {
renderFishLock.lock();
simUsages.startRender();
fishShown = 0;
simUsages.preSynchro(); // Notice: alternatives: use `SDL_GL_SetSwapInterval(0);`, or move `simUsages.startRender()` below the first use of `SdlGles2`
SdlGles2.glClear(SdlGles2.GL_COLOR_BUFFER_BIT); // Replaces `gc.clearRect(0, 0, resolution[0], resolution[1])`
simUsages.postSynchro();
for(Fish fish : fishList) {
if(fish.isVisible) { // For `Fish` not shown, this condition improves `SimUsages.fps` (lowers `SimUsages.renderNs`).
fishShown++;
fish.render();
}
}
SdlGles2.swapWindow(); // Presents the rendered frame (replaces implicit JavaFX frame commit).
simUsages.postRender();
renderFishLock.unlock();
}
/** Signals the main loop to exit, shuts down executor threads, and calls {@code SDL_Quit()} via {@link SdlGles2#destroy()}. Replaces {@code @Override stop()}. */
public void stop() {
quit = true;
scheduledExecutor.shutdownNow();
executor.shutdown();
SdlGles2.destroy(); /* Replaces implicit JavaFX window teardown. */
}
public class Fish { /* `static Fish` causes "{posBounds,posBounds.posBound()} cannot be referenced from a static context" (unless those are set to `static`, which prevents `FishSim` from use of separate values with multiple windows) */
public static Forces forcesSeparation = new Forces(22.0, 2.0);
public static Forces forcesSeparationNonsimilar = new Forces(100.0, 2.2);
public static Forces forcesAlignment = new Forces(100.0, 1.0);
public static Forces forcesCohesion = new Forces(100.0, 1.0);
public static Forces forcesBounds = new Forces(100.0, 2.0);
private static double dposMax = 3.0; // Motion lim (limit of derivative of position)
private static double d2Pos = 0.1; // Motion<sup>2</sup> (derivative #2 of position)
private static double isSimilarTolerance = 0.2;
public static boolean applyWallAvoidanceTru = (PosBounds.PosBoundsMode.wrapAroundResolution != posBounds.getPosBoundsMode());
public static boolean redFishAreAggressiveOrPoisonous = true; // changes how `isSimilarTo(Fish other)` uses `color.getRed()`
private Pos pos; // Position
private Pos dpos; // Motion (derivative of position)
private Color color; /* Replaces `javafx.scene.paint.Color`: uses `FishSim.Color` which supports `getRed()`, `getGreen()`, `getBlue()`. */
public boolean isInBounds;
public boolean isVisible = false; // Just stores `0 <= pos[0] && resolution[0] > pos[0] && 0 <= pos[1] && resolution[1] > pos[1]` for now.
public Fish(ImmutablePos pos, ImmutablePos dpos, Color color) {
this.pos = pos.clone(); // TODO: ensure this clones the actual (specialized) virtual function addresses, which improve CPU use
this.dpos = dpos.clone();
this.color = color;
}
public Fish(Pos pos, Pos dpos, Color color) { /* Notice: uses "placement moves" for {`pos`, `dpos`}. Gives `Fish` ownership of `pos`, ownership of `dpos`. */
this.pos = pos;
this.dpos = dpos;
this.color = color;
}
public boolean isSimilarTo(Fish o) {
// return color.equals(o.color); // Less CPU use, but requires that `fishList` has just a few colors.
// Color colorDis = Color.color(color.getRed() - o.color.getRed(), color.getGreen() - o.color.getGreen(), color.getBlue() - o.color.getBlue()); // Notice: `Color` is more intuitive to use, but was concerned that `java` will not fold this
double[] colorDis = {color.getRed() - o.color.getRed(), color.getGreen() - o.color.getGreen(), color.getBlue() - o.color.getBlue()};
if(redFishAreAggressiveOrPoisonous) {
colorDis[0] = Calculus.pow2(colorDis[0]);
}
// return isSimilarTolerance > (Math.hypot(Math.abs(colorDis[0]), Math.abs(colorDis[1]), Math.abs(colorDis[2]))); // [`Math.hypot()` still does not support > 2 dimensions?](https://esdiscuss.org/topic/how-about-more-args-for-math-hypot). Notice: if you use Euclidean distance, lower `isSimilarTolerance`.
return isSimilarTolerance > (Calculus.pow2(colorDis[0]) + Calculus.pow2(colorDis[1]) + Calculus.pow2(colorDis[2]));
} // TODO: Use a function (such as `javafx.scene.shape.Polygon.getPoints()`) for comparison of vertices. */
public Pos getPosDiff(Fish o) {
return posBounds.posDiff(pos, o.pos);
}
public synchronized void setPos(Pos newPos /* Notice: semantics of "placement move" */) {
isInBounds = posBounds.posBound(newPos); // If `PosBoundsMode.boundless != posBounds.getPosBoundsMode()`, this ensures the invariant `0 <= pos[dim] && PosBounds.getBounds()[dim] > pos[dim]` is established.
isVisible = (0 <= newPos.pos[0] && resolution[0] > newPos.pos[0] && 0 <= newPos.pos[1] && resolution[1] > newPos.pos[1]); // TODO: +`Pos::isLessOrEquals(Pos)`, +`Pos::isMore(pos)`
if((!isInBounds) && PosBounds.PosBoundsMode.boundless != posBounds.getPosBoundsMode()) {
outOfBounds("Fish::setPos", this);
}
pos = newPos; /* Notice: does not use `newPos.clone()` since this function is used in inner loops. After this function returns, `this` has ownership of `newPos`. */
}
public synchronized void setPos(ImmutablePos newPos) {
setPos(newPos.clone()); /* Notice: since this function is used in inner loops, `setPos(Pos newPos)` uses the semantics of "placement move", so if `newPos` is immutable, must clone. */
}
public synchronized void applyFlockingRules(List<Fish> allFish, List<Fish>[][] grid) {
int[] gridPos = {(int) (pos.pos[0] / gridResolution), (int) (pos.pos[1] / gridResolution)};
List<Fish> nearbyFish = new ArrayList<>();
// Check neighboring grid cells
if(PosBounds.PosBoundsMode.wrapAroundResolution == posBounds.getPosBoundsMode()) {
for(int i = gridPos[0] - 1; i <= gridPos[0] + 1; i++) {
for(int j = gridPos[1] - 1; j <= gridPos[1] + 1; j++) {
nearbyFish.addAll(grid[(i + grid.length) % grid.length][(j + grid[0].length) % grid[0].length]);
} /* TODO: move expensive `%`s into outer loop somehow. With `1000 == fishList.size() && 100 == monitorRefreshHertz`, 2 `%`s in inner loop executes 200,000 `%`/s. The most simple solution is to use outer branches to choose from loops which hardcode this, but thus duplicates codeflow. Does `java` do this for you if the loop uses most of the CPU? */
}
} else {
for(int i = Math.max(0, gridPos[0] - 1); i <= Math.min(grid.length - 1, gridPos[0] + 1); i++) {
for(int j = Math.max(0, gridPos[1] - 1); j <= Math.min(grid[0].length - 1, gridPos[1] + 1); j++) {
nearbyFish.addAll(grid[i][j]);
} /* TODO: move expensive `Math.{min,max}`s into outer loop somehow. With `1000 == fishList.size() && 100 == monitorRefreshHertz`, inner loop executes 200,000 `Math.{min,max}`/s */
}
}
applySeparation(nearbyFish);
applyAlignment(nearbyFish);
applyCohesion(nearbyFish);
if(applyWallAvoidanceTru) {
applyWallAvoidance(posBounds.getBounds());
}
}
private synchronized void applySeparation(List<Fish> nearbyFish) {
Pos sepDpos = dpos.zeros();
Pos sepNonsimilarDpos = dpos.zeros();
int count = 0, countNonsimilar = 0;
for(Fish o : nearbyFish) {
if(o != this) {
Pos posDiff = getPosDiff(o);
double dist = posDiff.magnitude(); // Notice: in future, +`Pos::boundHypotenus(o)`
if(isSimilarTo(o)) {
if(forcesSeparation.posIfDistScaleSum(sepDpos, posDiff, dist)) {
count++;
}
} else {
if(forcesSeparationNonsimilar.posIfDistScaleSum(sepNonsimilarDpos, posDiff, dist)) {
countNonsimilar++;
}
}
}
}
if(count > 0) {
sepDpos.slashEqualsScalar(count);
forcesSeparation.dposScaleSum(dpos, d2Pos, sepDpos);
}
if(countNonsimilar > 0) {
sepNonsimilarDpos.slashEqualsScalar(countNonsimilar);
forcesSeparationNonsimilar.dposScaleSum(dpos, d2Pos, sepNonsimilarDpos);
}
}
private synchronized void applyAlignment(List<Fish> nearbyFish) {
Pos avgDpos = dpos.zeros();
int count = 0;
for(Fish o : nearbyFish) {
if(o != this && isSimilarTo(o)) {
Pos posDiff = getPosDiff(o);
double distPow2 = posDiff.magnitudePow2();
if(forcesAlignment.posIfDistPow2Sum(avgDpos, o.dpos, distPow2)) {
count++;
}
}
}
if(count > 0) {
avgDpos.slashEqualsScalar(count);
forcesAlignment.dposScaleSum(dpos, d2Pos, avgDpos);
}
}
private synchronized void applyCohesion(List<Fish> nearbyFish) {
Pos avgPos = pos.zeros();
int count = 0;
for(Fish o : nearbyFish) {
if(o != this && isSimilarTo(o)) {
Pos posDiff = getPosDiff(o);
double distPow2 = posDiff.magnitudePow2();
if(forcesCohesion.posIfDistPow2Sum(avgPos, o.pos, distPow2)) {
count++;
}
}
}
if(count > 0) {
avgPos.slashEqualsScalar(count);
forcesCohesion.dposScaleSum(dpos, d2Pos, posBounds.posDiff(avgPos, pos));
}
}
private synchronized void applyWallAvoidance(double[] res) {
Pos avoidancePos = dpos.zeros();
if(pos.pos[0] < forcesBounds.distance) {
avoidancePos.pos[0] += (forcesBounds.distance - pos.pos[0]);
} else if(pos.pos[0] > res[0] - forcesBounds.distance) {
avoidancePos.pos[0] -= (pos.pos[0] - (res[0] - forcesBounds.distance));
}
if(pos.pos[1] < forcesBounds.distance) {
avoidancePos.pos[1] += (forcesBounds.distance - pos.pos[1]);
} else if(pos.pos[1] > res[1] - forcesBounds.distance) {
avoidancePos.pos[1] -= (pos.pos[1] - (res[1] - forcesBounds.distance));
}
avoidancePos.starEqualsScalar(d2Pos * forcesBounds.factor / forcesBounds.distance);
dpos.plusEquals(avoidancePos);
}
public synchronized void update() {
// Limit speed
double speed = dpos.magnitude();
if(speed > dposMax) {
dpos.slashEqualsScalar(speed);
dpos.starEqualsScalar(dposMax);
}
// Update position
setPos(pos.plus(dpos));
}
/**
* Renders this fish using GLES2 via {@link SdlGles2#drawFilledPolygon}.
* Replicates the JavaFX {@code gc.save/translate/rotate/setFill/beginPath/moveTo/lineTo/closePath/fill/restore} sequence.
* Fish shape vertices (local space): {@code {0,-10}, {-5,10}, {-2,0}, {2,0}, {5,10}}.
* Triangulated (triangle fan from vertex 0): triangles {@code {0,1,2}, {0,2,3}, {0,3,4}}.
*/
public synchronized void render() {
/* Replicate `gc.translate(tx,ty); gc.rotate(angleDeg+90)` as a 2-D rotation matrix. */
double angle = Math.atan2(dpos.pos[1], dpos.pos[0]) + Math.PI / 2.0; /* equiv. to Math.toRadians(Math.toDegrees(atan2) + 90) */
double cosA = Math.cos(angle);
double sinA = Math.sin(angle);
double tx = pos.pos[0];
double ty = pos.pos[1];
/* Fish shape local vertices: same coordinates as original JavaFX path */
final double[][] lv = {{0,-10}, {-5,10}, {-2,0}, {2,0}, {5,10}};
/* Triangulate polygon as fan from vertex 0: {0,1,2}, {0,2,3}, {0,3,4} -> 3 triangles, 9 verts, 18 floats */
final int[][] tris = {{0,1,2}, {0,2,3}, {0,3,4}};
float[] verts = new float[18];
int vi = 0;
for(int[] tri : tris) {
for(int idx : tri) {
double lx = lv[idx][0], ly = lv[idx][1];
verts[vi++] = (float)(lx * cosA - ly * sinA + tx); /* x' = x*cos - y*sin + tx */
verts[vi++] = (float)(lx * sinA + ly * cosA + ty); /* y' = x*sin + y*cos + ty */
}
}
SdlGles2.drawFilledPolygon(verts, (float)color.getRed(), (float)color.getGreen(), (float)color.getBlue(), 1.0f);
}
};
};
