Tutorials

Drawing thick outlines in OpenGL

Drawing outlines around 3D shapes is a common feature request, be it for selected items, quest markers, or what have you. In OpenGL there’s a few ways to do it and today I’d like to share with you the two methods I’ve tried – the OpenGL2 offset normals method (left) and the OpenGL3 per-pixel shader (right). Both use a variation on stencils. Stencils are like green screens. By isolating the thing you want to keep from the rest, it’s easy to do all kinds of neat tricks.

For those of you in the know, I’m using a brute force method. Skip to final thoughts for my reasoning.


OpenGL stencils

The first method I tried went something like this:

  • Draw every mesh
  • Draw selected stuff a second time, but this time to the stencil buffers
  • Lock the stencil buffer but mask everything – only draw to the non empty parts of the stencil.
  • Draw selected stuff a third time in the original buffer, but only edge lines in the selected color.

So let’s assume, for brevity, that there’s a drawAllMeshes method that works as advertised.
Somewhere in the pipeline there’s something like

drawAllMeshes(everything);
outlineSelectedMeshes(probablyLessThanEverything);

then outlineSelectedMeshes does all the stencil magic.

gl3.glEnable(GL3.GL_STENCIL_TEST);  // enable stencil testing  
gl3.glClear(GL3.GL_STENCIL_BUFFER_BIT | GL3.GL_DEPTH_BUFFER_BIT);  // clear the buffer  
gl3.glColorMask(false,false,false,false);  // disable writing to the color buffer  
gl3.glDepthMask(true);  // enable writing to the depth buffer  
gl3.glStencilMask(0xFF);  // enable writing to the stencil buffer  
gl3.glStencilFunc(GL3.GL_ALWAYS,1,0xFF);  // always pass the stencil test and set the value to 1  
gl3.glStencilOp(GL3.GL_KEEP,GL3.GL_KEEP,GL3.GL_REPLACE);  // set the stencil value to 1 when the depth test passes  

drawAllMeshes(gl3, selectedMeshes, camera, originShift);  // draw the selected meshes.  Only now they'll go to the stencil buffer.

gl3.glStencilFunc(GL3.GL_NOTEQUAL,1,0xFF);  // pass the stencil test if the value is not 1  
gl3.glStencilMask(0x00);  // disable writing to the stencil buffer  
gl3.glDepthMask(false);  // disable writing to the depth buffer  
gl3.glColorMask(true,true,true,true);  // enable writing to the color buffer 
gl3.glStencilOp(GL3.GL_KEEP,GL3.GL_KEEP,GL3.GL_KEEP);  // keep the stencil value  
gl3.glDisable(GL3.GL_CULL_FACE);  // draw both sides of the outline

gl3.glLineWidth(outlineThickness);  // set the line thickness
gl3.glPolygonMode(GL3.GL_FRONT_AND_BACK,GL3.GL_LINE);  // draw only lines

// outlineShader is an instance of ShaderProgram which gives me all kinds of convenience methods.
outlineShader.use(gl3);  
outlineShader.setMatrix4d(gl3, "viewMatrix", camera.getViewMatrix());  
outlineShader.setMatrix4d(gl3, "projectionMatrix", camera.getChosenProjectionMatrix(canvasWidth, canvasHeight));  
outlineShader.setColor(gl3, "outlineColor", Color.GREEN);  

// render the selected set with thick lines.
// drawAllMeshes would use the default shader, so I do it manually here
// to control the shader choice.
for(var mesh : selectedMeshes) {  
    outlineShader.setMatrix4d(gl3,"modelMatrix",mesh.getMatrix());  
    mesh.render(gl3);
}

gl3.glPolygonMode(GL3.GL_FRONT_AND_BACK,GL3.GL_FILL);  // reset shapes
gl3.glEnable(GL3.GL_CULL_FACE);  // restore settings
gl3.glDepthMask(true);  // restore settings
gl3.glLineWidth(1);  // restore settings
gl3.glStencilFunc(GL3.GL_ALWAYS,1,0xFF);  // turn off stencil testing  
gl3.glStencilOp(GL3.GL_KEEP, GL3.GL_KEEP, GL3.GL_REPLACE);  
gl3.glDisable(GL3.GL_STENCIL_TEST);

The outline shader v1

outline.vert was doing almost nothing.

#version 330 core  

layout(location = 0) in vec3 aPosition;  
layout(location = 1) in vec3 aNormal;  
layout(location = 2) in vec4 aColor;  
layout(location = 3) in vec2 aTexture;  

uniform mat4 projectionMatrix;  
uniform mat4 viewMatrix;  
uniform mat4 modelMatrix;

void main() {  
    vec3 offsetPosition = aPosition + aNormal * outlineSize;  
    vec4 worldPose = modelMatrix * vec4(offsetPosition, 1.0);  
    gl_Position = projectionMatrix * viewMatrix * worldPose;  
}

and outline.frag was doing truly nothing.

#version 330 core
out vec4 fragColor;  
uniform vec4 outlineColor;  // the outline color

void main() {  
    fragColor = outlineColor;
}

The effect on a basic cube looks like this:

Note that OpenGL also doesn’t give a way to adjust line end caps.

OpenGL 3 per pixel shaders

The OpenGL3 version required more setup but less work at run time. The process is:

  • Draw every mesh
  • Draw a second time to a stencil buffer FBO
  • Use the FBO as a reference texture (not the technical name)
  • Draw a rectangle over the whole screen.
  • The shader color every pixel in the rectangle near (but not on) the stencil.

Full screen rectangle

I’m sorely tempted to say “eh, I have a Mesh wrapper class, you don’t need these details”… but in the interest of being thorough, here’s the whole rectangle setup.

private final int [] rectangleVAO[1];
private final int [] rectangleVBO[1];

private void generateFullscreenQuad(GL3 gl3) {  
    float v = 1.0f;  // make smaller to check position on screen
    float[] quadVertices = {  
        // positions  // texcoords  
        -v, -v, 0f,   0f, 0f, // bottom-left  
         v, -v, 0f,   1f, 0f, // bottom-right  
        -v,  v, 0f,   0f, 1f, // top-left  
         v,  v, 0f,   1f, 1f  // top-right  
     };

    gl3.glGenVertexArrays(1, rectangleVAO, 0);  // make vao
    gl3.glBindVertexArray(VAO[0]);  // use vao
    gl3.glGenBuffers(2, rectangleVBO, 0);  // make vbos
    gl3.glBindBuffer(GL3.GL_ARRAY_BUFFER, rectangleVBO[0]);    // use vbos
    gl3.glBufferData(GL3.GL_ARRAY_BUFFER, quadVertices.length * Float.BYTES, FloatBuffer.wrap(quadVertices), GL3.GL_STATIC_DRAW);  // fill vbos
    gl3.glBindVertexArray(0);  // stop using vao
}

Frame buffer object

Normally the video card draws into an offscreen back buffer and at the right time swaps the back buffer and the front buffer. This way you don’t see all triangles being drawn one by one. We can create our own Frame buffer object (FBO) as canvas-sized textures. (The canvas is the whole drawing area, which might not be the entire window.) The video card can draw into the FBO of our choosing, and then we can do stuff with it. I couldn’t find an easy way to access the built-in stencil buffer so I did it this way.

// class parameters  
private int outlineThickness = 5;  
private final int [] stencilFBO = new int[1];
private final int [] stencilTexture = new int [1];

// width and height should be the canvas dimensions.
// every time the canvas changes size this should be called again.
private void setupStencilFramebuffer(GL3 gl3, int width, int height) {  
    deleteStencilBuffer(gl3);  
    // Create FBO if not already created  
    gl3.glGenFramebuffers(1, stencilFBO, 0);  
    // Create stencil texture if not created  
    gl3.glGenTextures(1, stencilTexture, 0);  

    gl3.glBindTexture(GL3.GL_TEXTURE_2D, stencilTexture[0]);  
    // create with a single 8-bit red channel
    gl3.glTexImage2D(GL3.GL_TEXTURE_2D, 0, GL3.GL_R8, width, height, 0, GL3.GL_RED, GL3.GL_UNSIGNED_BYTE, null);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_MIN_FILTER, GL3.GL_NEAREST);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_MAG_FILTER, GL3.GL_NEAREST);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_WRAP_S, GL3.GL_CLAMP_TO_EDGE);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_WRAP_T, GL3.GL_CLAMP_TO_EDGE);  

    // Bind the FBO and attach the stencil texture  
    gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, stencilFBO[0]);  
    gl3.glFramebufferTexture2D(GL3.GL_FRAMEBUFFER, GL3.GL_COLOR_ATTACHMENT0, GL3.GL_TEXTURE_2D, stencilTexture[0], 0);  
    gl3.glDrawBuffer(GL3.GL_COLOR_ATTACHMENT0);  

    // Check FBO status  
    int status = gl3.glCheckFramebufferStatus(GL3.GL_FRAMEBUFFER);  
    if (status != GL3.GL_FRAMEBUFFER_COMPLETE) {  
        throw new RuntimeException("Failed to setup stencil framebuffer: " + status);  
    }  

    // Unbind FBO  
    gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, 0);  
}

// called when the canvas changes size or it is disposed.
private void deleteStencilBuffer(GL3 gl3) {  
    if(stencilFBO[0]!=-1) {  
        gl3.glDeleteFramebuffers(1, stencilFBO,0);  
        stencilFBO[0] = -1;  
    }  
    if(stencilTexture[0]!=-1) {  
        gl3.glDeleteTextures(1, stencilTexture,0);  
        stencilTexture[0] = -1;  
    }  
}

Testing the stencil buffer

// Step 1: Render the stencil into an offscreen texture using the FBO  
gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, stencilFBO[0]);  
gl3.glViewport(0, 0, canvasWidth, canvasHeight);  
// Clear stencil texture  
gl3.glClearColor(0,0,0,0);  
gl3.glClear(GL3.GL_COLOR_BUFFER_BIT | GL3.GL_DEPTH_BUFFER_BIT);

// Please forgive my custom mesh shader here.
// Draw everything without lighting in flat white.
meshShader.use(gl3);  
meshShader.setMatrix4d(gl3, "viewMatrix", camera.getViewMatrix(originShift));  
meshShader.setMatrix4d(gl3, "projectionMatrix", camera.getChosenProjectionMatrix(canvasWidth, canvasHeight));  
meshShader.set1i(gl3, "useVertexColor", 0);  
meshShader.set1i(gl3, "useLighting", 0);  
meshShader.setColor(gl3,"diffuseColor",Color.WHITE);

for(var mesh : selectedMeshes) {  
    meshShader.setMatrix4d(gl3,"modelMatrix",mesh.getMatrix());
    mesh.render(gl3);
}

// resume editing the color buffer, do not change the depth mask or the stencil buffer.  
gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, 0);  

// Step 2: Render outlines using the stencil texture  
gl3.glActiveTexture(GL3.GL_TEXTURE0);  
gl3.glBindTexture(GL3.GL_TEXTURE_2D, stencilTexture[0]);  

captureTextureData(gl3,canvasWidth,canvasHeight);

captureTextureData is used to write stencilTexture to a BufferedImage, and then I can use a breakpoint to view the stencil buffer.

Of course this only works if something there is a selected mesh!
Also… it draws upside down because the screen Y axis is reverse from a BufferedImage y axis.

Drawing the quad

Once the stencil buffer looked good it was time to disable the capture and draw the quad on screen.

//captureTextureData(gl3,canvasWidth,canvasHeight);  

outlineShader.use(gl3);  
outlineShader.set1i(gl3, "stencilTexture", 0); // Texture unit 0  
outlineShader.set2f(gl3, "textureSize", canvasWidth, canvasHeight);  
outlineShader.setColor(gl3, "outlineColor", Color.GREEN);  
outlineShader.set1f(gl3, "outlineSize", outlineThickness);  

// Render the quad with the stencil texture to the screen  
gl3.glDisable(GL3.GL_CULL_FACE);  
fullScreenQuad.render(gl3);  
gl3.glEnable(GL3.GL_CULL_FACE);

All we need is…

Testing a new outline shader

outline.vert v2

#version 330 core  

layout(location = 0) in vec3 aPosition;  
layout(location = 1) in vec2 aTexture;  

void main() {  
    gl_Position = vec4(position, 1.0);  
}

outline.frag v2

#version 330 core  

uniform vec4 outlineColor = vec4(0.0, 1.0, 0.0, 1.0);  
uniform float outlineSize = 1.0;  
uniform vec2 canvasSize;  // Size of the texture/screen  

out vec4 finalColor;  // Output fragment color  

// Sampler for the stencil texture (or depth-stencil data)  
uniform sampler2D stencilTexture;  

// the screen and the stencilTexture have dimensions from (-1,-1) to (1,1)  
// the canvasSize, outlineSize, and gl_FragCoord are in pixels from (0,0) to (width,height)  
void main() {  
    // Size of a fragment in texture coordinates  
    vec2 texelSize = 1.0 / canvasSize;  
    // Current pixel position in the stencil texture  
    vec2 textureCoord = gl_FragCoord.xy / canvasSize;  
    vec4 stencilValue = texture(stencilTexture, textureCoord);  

    // If the stencil value is not zero we're inside the stencil area so skip.  
    if (stencilValue.r > 0.0) discard;  

    int outInt = int(ceil(outlineSize));  
    float o2 = outlineSize * outlineSize;  
    // loop over all pixels within +/-outline size  
    for (int y = -outInt; y <= outInt; y++) {  
        for (int x = -outInt; x <= outInt; x++) {  
            if(x*x + y*y > o2) continue; // Skip pixels outside the circle  
            // convert pixel offset to texture coordinate offset
            vec2 offset = vec2(x, y) * texelSize;  
            // Sample the stencil texture at the offset position  
            vec4 neighbor = texture(stencilTexture, textureCoord + offset);  
            if(neighbor.r > 0.0) {  
                // We're in range, set the outline color and exit  
                finalColor = outlineColor;  
                return;  
            }  
        }  
    }  
    // If no neighboring pixels are found with a stencil value do nothing.
    //finalColor = vec4(0,0,1,1);  // make the background blue for testing
    //finalColor = vec4(0,(textureCoord.x+1)/2,(textureCoord.y+1)/2,1);
    // gradient for testing
}

Tada!

Final thoughts

The approach I took is considered “Brute force”. Ben Golus’ Jump Flood Algorithm (JFA) is the final answer for most people. It’s the hardest to do and the most efficient at run time. Presently I am not doing huge outlines that would require the run time cost savings. Also I have not seen a GLSL step-by-step… so… Perfect is the enemy of good enough.

I write 3D stuff mainly to simulate robots I want to build. The app is called Robot Overlord and it’s totally open source, so feel free to click the link and join in the latest fun. Show the world how smart you are and make a PR with working JFA.

Seriously, if you know someone learning OpenGL… make them start with 2.0. If you can choose the language for them, pick Java. It’s just so much easier and more likely to succeed. Watching my nephew do a ten chapter tutorial on Vulkan for one triangle… that’s crazy pants.

News Projects

Friday Facts 20: Java Swing Dial UX

Dial is a Java Swing component designed to create a customizable dial interface. This component allows users to interact using the mouse wheel, click-and-drag actions, or keyboard keys. It features an ActionListener to handle turn commands, making it easy to integrate into various Java applications that require a rotary input method. It can be sometimes be more intuitive than a JSlider, which cannot “roll over” back to the starting value.

Key Features

  • Mouse Wheel Interaction: Turn the dial smoothly with the mouse wheel.
  • Mouse Click+Drag: Click and drag to adjust the dial.
  • Keyboard Control: Use the +/- keys to increment or decrement the dial value.
  • Rollover: Unlike JSlider, the dial can wrap around back to the start. Great for controlling an angle value.

Basic Usage

import com.marginallyclever.dial.Dial;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class DialDemo {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Dial Demo");
        Dial dial = new Dial();
        
        dial.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Dial turned: " + dial.getValue());
            }
        });

        frame.add(dial);
        frame.setSize(200, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

Final Thoughts

You can customize the appearance and behavior of the dial through its properties. Adjust the color, range, and initial value as needed to fit your application.

For detailed documentation, visit the GitHub repository.

News

Friday Facts 7: A 2D physics engine in Processing (Java) and the Sunk Cost Fallacy

I have a robot dog being operated on by the Students at Alpha Secondary school in Burnaby, BC, as part of a work experience program. The goal is to make the dog walk as good or better than Spot Micro. Obviously this is not done in a weekend. How do you eat an elephant? One piece at a time. The current plan is to tackle things in order:

  • get it to turn on (done!)
  • move each motor individually (done!)
  • move them together (done!)
  • make it stand up and sit down (working on it!)
  • teach it to roll over and recover from a fall
  • teach it to walk

In a sense I’ve delegated all these steps to the students. Which frees me to do the part that needs experience – making tools that will empower them to do the hard steps at the end.

I don’t want to break the robot because it’s expensive to fix. Therefore I don’t want anyone to drop the robot hundreds of times while testing it. Given the tools I have, what can I do to test the robot without dropping the robot? The same thing I’ve done with all my robots – simulate them! But I’ve never used a phsyics system in Robot Overlord before.

I already have all my code and development tools in Java, so my first instinct is to continue in that direction. I didn’t find any up-to-date physics engines that work in Java. JBullet hasn’t been updated since 2016. Other engines I found were Java ports of C libraries. I don’t have links to them any more but those I found were stale and had no “contact us” system. Two solutions present themselves:

  1. Write my own physics system in Robot Overlord from scratch
  2. Install Gazebo on a linux system, ?, profit.

For your consideration, this is my attempt at number 1.

2D physics with circles and boxes in Processing

This is the first time I’ve had to simulate things with physics in Java.

This is a 2D physics engine written in Processing 4.0b. At tag 1.0, it looks like this:

Much help and inspiration was obtained by reading the code for Impulse Engine by Randy Gaul. Do your homework!

It took a day to write the drawing of shapes and 13 more to debug the physics up to this point. Here’s what it looked like at the half-way point:

The hope is to extend this into 3D, add constraints (think bones connected by joints), add friction, and then make physically approximate ragdoll models of the Spot Mini. They can be dropped, kicked, drop kicked, and beat up over and over until the simulated walking gets them up and balanced. Then the simulation can be tied to the real machine and our goal should be within sight if not in reach.

The sunk cost fallacy

Initially I didn’t even consider option no 2. The cost of setting up a linux box or partition, learning Gazebo, installing the robot model, unknown further mysteries tbd… I choose the devil I know over the devil I don’t!

Discord user M2710 DM’d me to ask “why don’t you just [gazebo]?” and made a compelling case that it might be easier than running my own physics engine. I couldn’t immediately prove the idea wrong so I have to consider it might be right. I’ve booked a day in the near future to try Gazebo and Webots. Look for that in an upcoming FF.

Hiring

If you have experience with Gazebo or Webots, share your story with us on the Discord channel. we really want to eat your juicy brains know what you know.

If you have java/physics skills and you think you can get the Robot Overlord features (3D, constraints, friction, physically approximate ragdoll models of the Spot Mini) working, DM me! I want to help you help me. Make me an offer, I’m eager. I only have so many hours in the day and I need your help.

News

Make a DND Discord Dice Roller in Java (with regular expressions!)

Discord is a popular online service for gaming that really took off during the 2020’s pandemic. My friends and I play Dungeons and Dragons and we wanted a way to roll dice online without giving all our data to some wierdo company. So, being that I’m good with code, I wrote my own. Here’s my steps so that you can learn from what I did. We’re also going to use a really powerful tool called regular expressions to make reading dice requests super easy.

On the shoulders of giants

Writing my own code from scratch and figuring things out is fun and educational, but I’m getting old and I don’t have a lot of time left. To save time I used Java, available libraries, and a slightly out of date tutorial by Oliy Barrett. I recommend you read this for the basics.

Hello, World!

When using someone else’s library there’s always a bit of setup and teardown. Here’s the essentials.

token.txt is the private token for this bot. Never share it. If you check it into Github then Discord will send you a friendly email telling you the token has been rejected forever and you have to make a new one. use your .gitignore to make sure that never happens.

public class DiscordDND extends ListenerAdapter {
    static final String MY_ENTITY_ID = "***";
    static final String MY_ENTITY_NAME = "Dice Roller";
    static public final String ROLL_COMMAND = "~r";

    public static void main( String[] args ) throws LoginException {
        String token = readAllBytesFromFile(DiscordDND.class.getResource("token.txt"));
        JDA jda = JDABuilder.createDefault(token).build();
        jda.addEventListener(new DiscordDND());
    }

    private static String readAllBytesFromFile(URL filePath) {
        String content = "";
        try {
            System.out.println("Token search: "+filePath.toURI());
            content = new String ( Files.readAllBytes( Paths.get( filePath.toURI() ) ) );
        }  catch (Exception e) {
            e.printStackTrace();
	}
        return content;
    }

    @Override
    public void onMessageReceived(MessageReceivedEvent event) {
    	if(event.getAuthor().isBot()) return;

    	String message = event.getMessage().getContentDisplay();
    	
    	if(!message.startsWith(ROLL_COMMAND)) return;
    	// remove the prefix and continue
    	message = message.substring(ROLL_COMMAND.length());

        System.out.println("I heard: "+message);

        // handle the roll here
    }
}

When the app runs it loads the token, connects to Discord with the token, and gets ready to listen to things being said in the discord servers to which it has been invited. How do you invite a bot to a server? Uh… I don’t remember.

Understanding a roll request

The regular expression syntax for a dnd dice roll

The normal format for writing out a dice roll in DND is [number of dice]d(number of sides)[k(number to keep)][+/-(modifier)] with no spaces. I found a regular expression online that pretty closely matches this pattern and modified it.

  • Anything in a () is a group.
  • ? means “zero or one of the previous element” (in this case, the previous group).
  • [\+\-] means “any characters inside the [] braces”. Combined with the ? it means “at most one + or – symbol.” Because of text formatting rules in regular expressions the + and - have to escaped by putting a backslash \ in front of them.
  • \d means digit. \d+ means 1 or more digits.

So putting it all together it says:

  • The first group has a positive or negative whole number. The group is optional.
  • The second group starts with the letter ‘d’, then a positive or negative whole number. The group is required.
  • The third group starts with the letter ‘k’ and then a positive or negative whole number. The group is optional.
  • The fourth group MUST start with +/- and then a whole number. The group is optional.

Hey! Why negative dice and negative sides? It’s not that I want negative dice. But sometimes users type silly things. I thought it would be fun to catch those and deal with them in equally funny ways.

Why keep negative dice? In DND sometimes the player rolls with advantage to keep the highest dice and sometimes they roll with disadvantage to keep the lowest dice. Negative numbers mean keep the low dice.

To use regular expressions in Java (or OpenJDK) I’m using the Pattern and Matcher classes.

// remove all whitespace and the roll command from the start
String saneMessage = sanitizeMessage(event.message);

Pattern p = Pattern.compile("([\\+\\-]?\\d+)?(d[\\+\\-]?\\d+)(k[\\+\\-]?\\d+)?([\\+\\-]\\d+)?");
Matcher m = p.matcher(saneMessage);
id(m.find()) {
	int numDice=1, numSides=20, numKeep, modifier=0;
	if(m.group(1) !=null && !m.group(1).isEmpty()) numDice  = Integer.parseInt(m.group(1));
	if(m.group(2) !=null && !m.group(2).isEmpty()) numSides = Integer.parseInt(m.group(2).substring(1));
	if(m.group(3) !=null && !m.group(3).isEmpty()) numKeep  = Integer.parseInt(m.group(3).substring(1));
	else numKeep=numDice;
	if(m.group(4) !=null && !m.group(4).isEmpty()) modifier = Integer.parseInt(m.group(4));

	roll(event,numDice,numSides,numKeep,modifier);
	return;
}

In Java Strings the \ symbol is special so I have to escape them again – the double backslash is not a mistake.

Matcher returns the original expression in m.group(0). If Matcher does not find a group it returns null for that group index. That means I can reliably expect group 4 is always the modifier and so on.

Rolling and keeping

int [] rolls = rollDice(numDice,numSides);
if(numKeep!=numDice) {
	if(numKeep>0) keepSomeHighRolls(rolls,numKeep);
	else keepSomeLowRolls(rolls,-numKeep);
}
event.reply(event.actorName + ": "+renderResults(rolls,modifier));

The catch here is that I don’t want to sort the rolls into high > low because it would look wrong to the user. Organic rolls do not happen that way! I’m stuck searching for the worst roll, numRolls – numKeep times.

for(int k = numKeep;k<rolls.length;++k) {
	int worst = 0;
	for(int i=0;i<rolls.length;++i) {
		if(rolls[i]>0 && rolls[worst]>rolls[i]) worst = i;
	}
	rolls[worst] *= -1;  // mark it as rejected but keep the value.
}

Final thoughts

As a final bit of flair I add a meme pic for natural 20 and natural 1 rolls. If a natural 1 is not kept I put in Neo’s famous bullet dodge from The Matrix. What would be a good meme for a not-kept-natural-20?

All the code for this project can be found at https://github.com/i-make-robots/DiscordDND/. It’s got way more stuff!

Opinion Projects

Robot Overlord: A puzzle in Java

Robot Overlord Java app contains lots and lots of classes, some of which are robots and their gui.

I want robot developers to have an easy time adding their robots to RO. A simple interface, minimal distraction, and examples to work from are Good Things. I’m told that RO can use a Service Provider Interface (SPI) to load a jar it’s never seen before, a jar that contains an implementation of the interface. I would then

  • make a RobotInterface,
  • make every robot I’ve already got use that interface
  • move each robot to a separate jar and load said jars through SPI
  • make a separate github project for each robot
  • advertise these plugins via tutorials so that you can fork a repo, adjust to taste, and publish your new thing.

What I’m discovering is that SPI is tricky tricky.

  • I can’t find any online examples where someone has done this successfully.
  • I have not yet got RO to load my first robot’s jar file, tho I’m trying. Is the jar packaged wrong? Maybe it doesn’t say “yes, I have that interface!” in the right way.
  • Is RO not even seeing the jar? I’m told SPI looks for any jar on the classpath. I printed out the classpath, then put the robot jar in one of those classpath folders and ran the app again. Nothing.

There are several possible points of failure, none of which can be clearly eliminated as possibilities. Worse, I’m not sure how these plugins would be debugged. Running RO would not give a lot of insight into the plugins’ inner workings. Would I still be able to tweak code in real time? That is a must.

So I ask you, dear reader: am I way off track? What do?

I should note here that I do not want to have to run RO from the command line with a custom classpath. While I’m able to do it, I doubt that the people who buy robots and use them will even know how to open a command line. Imagine a grade school teacher trying to set up for their students, or your aged mother who’s used to OSX. It ain’t happening. You don’t want that tech support phone call and neither do I.