Author Topic: ShadowHelper Code Never Completes  (Read 5643 times)

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #15 on: September 02, 2012, 05:25:16 am »
OK, I just got around to porting the AdvancedExample, per your suggestion. "Got here.0" gets printed, "Got here" does not (so we're back to the shadow code not completing). Gotta be a bug (or "issue") of some kind. Please run this for me and see for yourself.

Code: [Select]
import java.awt.*;
import java.awt.event.*;

import com.threed.jpct.*;
import com.threed.jpct.util.*;

import org.lwjgl.input.*;

public class AdvancedExample extends Frame implements WindowListener, IPaintListener {
     private static final long serialVersionUID = 1L;
     private static float PI = (float) Math.PI;

     private KeyMapper keyMapper = null;
     private MouseMapper mouseMapper = null;

     private FrameBuffer buffer = null;
     private World world = null;
     private World sky = null;
     private Object3D plane = null;
     private Object3D snork = null;
     private Object3D rock = null;
     private Object3D dome = null;
     private Light sun = null;

     private ShadowHelper sh = null;
     private Projector projector = null;

     private float xAngle = 0;

     private boolean forward = false;
     private boolean backward = false;
     private boolean up = false;
     private boolean down = false;
     private boolean left = false;
     private boolean right = false;

     private float ind = 0;
     private boolean doLoop = true;
     private int fps = 0;
     private long time = System.currentTimeMillis();
     private Ticker ticker = new Ticker(15);

     private Canvas glCanvas;

     public void windowClosing(WindowEvent e) {
if (e.getWindow() == this)
     doLoop = false;
     }
     public void windowClosed(WindowEvent e) {}
     public void windowOpened(WindowEvent e) {}
     public void windowActivated(WindowEvent e) {}
     public void windowDeactivated(WindowEvent e) {}
     public void windowIconified(WindowEvent e) {}
     public void windowDeiconified(WindowEvent e) {}

     public static void main(String[] args) throws Exception {
Config.glVerbose = true;
AdvancedExample cd = new AdvancedExample();
cd.init();
cd.gameLoop();
     }

     public AdvancedExample() {
Config.glAvoidTextureCopies = true;
Config.maxPolysVisible = 1000;
Config.glColorDepth = 24;
Config.glFullscreen = false;
Config.farPlane = 4000;
Config.glShadowZBias = 0.8f;
Config.lightMul = 1;
Config.collideOffset = 500;
Config.glTrilinear = true;
     }

     public void finishedPainting() {
fps++;
     }

     public void startPainting() {
     }

     private void init() throws Exception {
this.setTitle("AdvancedExample AwtGL Edition");
// Load textures
TextureManager tm = TextureManager.getInstance();
tm.addTexture("grass", new Texture("example/GrassSample2.jpg"));
tm.addTexture("disco", new Texture("example/disco.jpg"));
tm.addTexture("rock", new Texture("example/rock.jpg"));
tm.addTexture("normals", new Texture("example/normals.jpg"));
tm.addTexture("sky", new Texture("example/sky.jpg"));

// Initialize frame buffer
buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_NORMAL);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
glCanvas = buffer.enableGLCanvasRenderer();
this.add(glCanvas);
buffer.setPaintListener(this);

// Initialize worlds
world = new World();
sky = new World();
world.setAmbientLight(30, 30, 30);
sky.setAmbientLight(255, 255, 255);

world.getLights().setRGBScale(Lights.RGB_SCALE_2X);
sky.getLights().setRGBScale(Lights.RGB_SCALE_2X);

// Initialize mappers
keyMapper = new KeyMapper(glCanvas);
mouseMapper = new MouseMapper(buffer);
mouseMapper.hide();

// Load/create and setup objects
plane = Primitives.getPlane(20, 30);
plane.rotateX(PI / 2f);
plane.setSpecularLighting(true);
plane.setTexture("grass");
plane.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS);

rock = Loader.load3DS("example/rock.3ds", 15f)[0];
rock.translate(0, 0, -90);
rock.rotateX(-PI / 2);
TextureInfo stoneTex = new TextureInfo(tm.getTextureID("rock"));
stoneTex.add(tm.getTextureID("normals"), TextureInfo.MODE_MODULATE);
rock.setTexture(stoneTex);
rock.setSpecularLighting(true);

snork = Loader.loadMD2("example/snork.md2", 0.8f);
snork.translate(0, -25, -50);
snork.setTexture("disco");

dome = Object3D.mergeAll(Loader.load3DS("example/dome.3ds", 2));
dome.build();
dome.rotateX(-PI / 2f);
dome.setTexture("sky");
dome.calcTextureWrap();
tileTexture(dome, 3);
dome.translate(plane.getTransformedCenter().calcSub(dome.getTransformedCenter()));
dome.setLighting(Object3D.LIGHTING_NO_LIGHTS);
dome.setAdditionalColor(Color.WHITE);

// Add objects to the worlds
world.addObject(plane);
world.addObject(snork);
world.addObject(rock);
sky.addObject(dome);

// Build all world's objects
world.buildAllObjects();

// Compile all objects for better performance
plane.compileAndStrip();
rock.compileAndStrip();
dome.compileAndStrip();

snork.compile(true, true, true, false, 2000);
snork.setCollisionMode(Object3D.COLLISION_CHECK_SELF);

// Deform the plane
Mesh planeMesh = plane.getMesh();
planeMesh.setVertexController(new Mod(), false);
planeMesh.applyVertexController();
planeMesh.removeVertexController();

// Initialize shadow helper
projector = new Projector();
projector.setFOV(1.5f);
projector.setYFOV(1.5f);
System.out.println("Got here. 0");
sh = new ShadowHelper(world, buffer, projector, 2048);
sh.setCullingMode(false);
sh.setAmbientLight(new Color(30, 30, 30));
sh.setLightMode(true);
sh.setBorder(1);

sh.addCaster(snork);
sh.addCaster(rock);
sh.addReceiver(plane);

// Setup dynamic light source
sun = new Light(world);
sun.setIntensity(250, 250, 250);
sun.setAttenuation(800);

// Setup shaders for the rock
String fragmentShader = Loader.loadTextFile("example/shader/fragmentshader.glsl");
String vertexShader = Loader.loadTextFile("example/shader/vertexshader.glsl");

GLSLShader shader = new GLSLShader(vertexShader, fragmentShader);
// shader.setShadowHelper(sh);
shader.setStaticUniform("colorMap", 0);
shader.setStaticUniform("normalMap", 1);
shader.setStaticUniform("invRadius", 0.0005f);
rock.setRenderHook(shader);

// Move camera
Camera cam = world.getCamera();
cam.moveCamera(Camera.CAMERA_MOVEOUT, 150);
cam.moveCamera(Camera.CAMERA_MOVEUP, 100);
cam.lookAt(plane.getTransformedCenter());
cam.setFOV(1.5f);
System.out.println("Got here.");
this.setSize(800, 600);
this.setVisible(true);
     }

private void pollControls() {
KeyState ks = null;
while ((ks = keyMapper.poll()) != KeyState.NONE) {
if (ks.getKeyCode() == KeyEvent.VK_ESCAPE) {
doLoop = false;
}

if (ks.getKeyCode() == KeyEvent.VK_UP) {
forward = ks.getState();
}

if (ks.getKeyCode() == KeyEvent.VK_DOWN) {
backward = ks.getState();
}

if (ks.getKeyCode() == KeyEvent.VK_LEFT) {
left = ks.getState();
}

if (ks.getKeyCode() == KeyEvent.VK_RIGHT) {
right = ks.getState();
}

if (ks.getKeyCode() == KeyEvent.VK_PAGE_UP) {
up = ks.getState();
}

if (ks.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
down = ks.getState();
}
}

if (org.lwjgl.opengl.Display.isCloseRequested()) {
doLoop = false;
}
}

private void move(long ticks) {

if (ticks == 0) {
return;
}

// Key controls

SimpleVector ellipsoid = new SimpleVector(5, 5, 5);

if (forward) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEIN,
ellipsoid, ticks, 5);
}

if (backward) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEOUT,
ellipsoid, ticks, 5);
}

if (left) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVELEFT,
ellipsoid, ticks, 5);
}

if (right) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVERIGHT,
ellipsoid, ticks, 5);
}

if (up) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEUP,
ellipsoid, ticks, 5);
}

if (down) {
world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEDOWN,
ellipsoid, ticks, 5);
}

// mouse rotation

Matrix rot = world.getCamera().getBack();
int dx = mouseMapper.getDeltaX();
int dy = mouseMapper.getDeltaY();

float ts = 0.2f * ticks;
float tsy = ts;

if (dx != 0) {
ts = dx / 500f;
}
if (dy != 0) {
tsy = dy / 500f;
}

if (dx != 0) {
rot.rotateAxis(rot.getYAxis(), ts);
}

if ((dy > 0 && xAngle < Math.PI / 4.2)
|| (dy < 0 && xAngle > -Math.PI / 4.2)) {
rot.rotateX(tsy);
xAngle += tsy;
}

// Update the skydome

sky.getCamera().setBack(world.getCamera().getBack().cloneMatrix());
dome.rotateY(0.00005f * ticks);
}

     private void gameLoop() throws Exception {
SimpleVector pos = snork.getTransformedCenter();
SimpleVector offset = new SimpleVector(1, 0, -1).normalize();

long ticks = 0;

while (doLoop) {
ticks = ticker.getTicks();
if (ticks > 0) {
// animate the snork and the dome

animate(ticks);
offset.rotateY(0.007f * ticks);

// move the camera
pollControls();
move(ticks);
}

// update the projector for the shadow map
projector.lookAt(plane.getTransformedCenter());
projector.setPosition(pos);
projector.moveCamera(new SimpleVector(0, -1, 0), 200);
projector.moveCamera(offset, 215);
sun.setPosition(projector.getPosition());

// update the shadow map
sh.updateShadowMap();

// render the scene
buffer.clear();

buffer.setPaintListenerState(false);
sky.renderScene(buffer);
sky.draw(buffer);
buffer.setPaintListenerState(true);
sh.drawScene();
buffer.update();
buffer.displayGLOnly();
glCanvas.repaint();

// print out the fps to the console
if (System.currentTimeMillis() - time >= 1000) {
System.out.println(fps);
fps = 0;
time = System.currentTimeMillis();
}
}
buffer.dispose();
this.dispose();
System.exit(0);
     }

private void animate(long ticks) {
if (ticks > 0) {
float ft = (float) ticks;
ind += 0.02f * ft;
if (ind > 1) {
ind -= 1;
}
snork.animate(ind, 2);
snork.rotateY(-0.02f * ft);
snork.translate(0, -50, 0);
SimpleVector dir = snork.getXAxis();
dir.scalarMul(ft);
dir = snork.checkForCollisionEllipsoid(dir, new SimpleVector(5, 20, 5), 5);
snork.translate(dir);
dir = snork.checkForCollisionEllipsoid(new SimpleVector(0, 100, 0), new SimpleVector(5, 20, 5), 1);
snork.translate(dir);
}
}

private void tileTexture(Object3D obj, float tileFactor) {
PolygonManager pm = obj.getPolygonManager();

int end = pm.getMaxPolygonID();
for (int i = 0; i < end; i++) {
SimpleVector uv0 = pm.getTextureUV(i, 0);
SimpleVector uv1 = pm.getTextureUV(i, 1);
SimpleVector uv2 = pm.getTextureUV(i, 2);

uv0.scalarMul(tileFactor);
uv1.scalarMul(tileFactor);
uv2.scalarMul(tileFactor);

int id = pm.getPolygonTexture(i);

TextureInfo ti = new TextureInfo(id, uv0.x, uv0.y, uv1.x, uv1.y,
uv2.x, uv2.y);
pm.setPolygonTexture(i, ti);
}
}

     private static class MouseMapper {
private boolean hidden = false;
private int height = 0;

public MouseMapper(FrameBuffer buffer) {
height = buffer.getOutputHeight();
init();
}

public void hide() {
     if (!hidden) {
//HIDE CURSOR
hidden = true;
     }
}

public void show() {
     if (hidden) {
//UNHIDE CURSOR
hidden = false;
     }
}

public boolean isVisible() {
return !hidden;
}

public void destroy() {
}

public boolean buttonDown(int button) {
return false;
}

public int getMouseX() {
// return Mouse.getX();
     return 0;
}

public int getMouseY() {
// return height - Mouse.getY();
     return 0;
}

public int getDeltaX() {
     return 0;
}

public int getDeltaY() {
     return 0;
}

private void init() {

}
     }

private static class Mod extends GenericVertexController {
private static final long serialVersionUID = 1L;

public void apply() {
SimpleVector[] s = getSourceMesh();
SimpleVector[] d = getDestinationMesh();
for (int i = 0; i < s.length; i++) {
d[i].z = s[i].z
- (10f * ((float) Math.sin(s[i].x / 50f) + (float) Math.cos(s[i].y / 50f)));
d[i].x = s[i].x;
d[i].y = s[i].y;
}
}
}

private static class Ticker {

private int rate;
private long s2;

public static long getTime() {
return System.currentTimeMillis();
}

public Ticker(int tickrateMS) {
rate = tickrateMS;
s2 = Ticker.getTime();
}

public int getTicks() {
long i = Ticker.getTime();
if (i - s2 > rate) {
int ticks = (int) ((i - s2) / (long) rate);
s2 += (long) rate * ticks;
return ticks;
}
return 0;
}
}
}

Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #16 on: September 02, 2012, 08:08:54 pm »
There's no issue, you just have the wrong order. Like i posted before:

What you are supposed to do:

  • enable the GLCanvasRenderer on the FrameBuffer
  • add the canvas to your frame/component
  • make your frame/component visible and active
  • setup your ShadowHelper

You have to make the component visible BEFORE setting up the ShadowHelper or otherwise, it will never terminate, because it waits for a drawing event that will never happen. This might feel clunky, but the design of LWJGL's Canvas enforces it.

Just take the setSize/setVisible-lines and move them right before you create the ShadowHelper. And remove the call to Display.isCloseRequested in addition...it doesn't work in a non-native window, because there is no Display being created.

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #17 on: September 03, 2012, 01:30:43 am »
OK, so it works and the shadows are visible. I'm thinking my world might be too small (I'm exporting the OBJ level at a scale of .06). Could that be it? If so, other than scaling everything up, is there a workaround?

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #18 on: September 03, 2012, 02:24:26 am »
When is the "VBO created" message printed? Could the lack of visible shadows come from the fact that the "VBO created" messages are happening after setupShadows()?

Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #19 on: September 03, 2012, 11:16:30 am »
VBOs will be created when the mesh data gets uploaded to the GPU. It has nothing to do with shadows and you can even disable it in Config if you want to.

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #20 on: September 03, 2012, 01:49:11 pm »
What about the other part of my question (the size )?

Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #21 on: September 03, 2012, 05:52:04 pm »
I'm not sure about the size....it might matter somehow but i don't think that it can cause shadows to disappear. Have you checked you far plane setting? Maybe it's to narrow so that the scenes gets clipped when viewed from the light source?

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #22 on: September 16, 2012, 08:13:49 pm »
Two things:
1) the following code works (it's the exact same code as is in the program where it doesn't). So is it possible that some models can't be shadow receivers (perhaps something about their normals)?
2) 2 new VisibilityLists are being created. You keep telling me it's about swallowing Exceptions, but it never was for me, and this almost always happens.

By the way, if instead of translating the cube by -3f on y I translate it by -1f, the shadow becomes VERY "edgy" (very large squares).

Code: [Select]
import java.awt.*;
import java.awt.event.*;
import com.threed.jpct.*;
import com.threed.jpct.util.*;

public class ShadowTest extends Frame implements WindowListener {
     private boolean keepGoing;
     private ShadowHelper sh;
     private Object3D ground, hero;
     private World theWorld;
     private FrameBuffer buffer;
     private Canvas glCanvas;
     private Projector projector;
     private Camera theCamera;
     private Light sun;
     public ShadowTest() {
this.setTitle("BR's");
theWorld = new World();
ground = Primitives.getPlane(10, 20f);
ground.rotateX((float)Math.PI*.5f);
hero = Primitives.getCube(5);
theWorld.addObject(ground);
theWorld.addObject(hero);
theWorld.buildAllObjects();
hero.translate(0, -3, 0);
buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_HARDWARE_ONLY);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
glCanvas = buffer.enableGLCanvasRenderer();
theCamera = theWorld.getCamera();
theCamera.setPosition(hero.getTransformedCenter());
theCamera.moveCamera(Camera.CAMERA_MOVEOUT, 20f);
this.add(glCanvas);
this.addWindowListener(this);
this.setSize(800, 600);
this.setVisible(true);
setupShadows();
gameLoop();
     }
     private void setupShadows() {
Config.glDynamicBatchSize = 4000;
projector = new Projector();
projector.setFOV(1.5f);
projector.setYFOV(1.5f);
sh = new ShadowHelper(theWorld, buffer, projector, 1024);
sh.setCullingMode(false);
sh.setAmbientLight(new Color(55, 55, 55));
sh.setLightMode(true);
sh.setBorder(1);
sh.addCaster(hero);
sh.addReceiver(ground);
sun = new Light(theWorld);
sun.setIntensity(255, 255, 255);
sun.setAttenuation(800);//800
theCamera.setFOV(1.5f);
glCanvas.repaint();
super.paint(this.getGraphics());
System.out.println("\n\n\nFinished shadows. Initialized properly? "+sh.isInitialized() +"\n\n");
     }
     private void gameLoop() {
keepGoing = true;
while (keepGoing) {
     draw();
     Thread.yield();
}
buffer.dispose();
this.dispose();
System.exit(0);
     }
     private void draw() {
buffer.clear();

if (!doShadows()) {
     theWorld.renderScene(buffer);
     theWorld.draw(buffer);
}
buffer.update();
buffer.displayGLOnly();
glCanvas.repaint();
     }
     private boolean doShadows() {
if (sh == null)
     return false;
projector.lookAt(ground.getTransformedCenter());
SimpleVector position = hero.getTransformedCenter();
position.z += 40f;
projector.setPosition(position);//hero.getRoot().getTransformedCenter()
projector.moveCamera(new SimpleVector(0, -1, 0), 30);//200
sun.setPosition(projector.getPosition());
sh.updateShadowMap();
sh.drawScene();
return true;
     }

     public void windowClosing(WindowEvent e) {
if (e.getWindow() == this)
     keepGoing = false;
     }
     public void windowClosed(WindowEvent e) {}
     public void windowOpened(WindowEvent e) {}
     public void windowIconified(WindowEvent e) {}
     public void windowDeiconified(WindowEvent e) {}
     public void windowActivated(WindowEvent e) {}
     public void windowDeactivated(WindowEvent e) {}

     public static void main(String[] args) {
new ShadowTest();
     }
}

Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #23 on: September 16, 2012, 09:32:10 pm »
Every model can be a shadow receiver. There's no relation to normals in any way. What is required is that the model has to be able to take the addional texture layer that the ShadowHelper adds to it, but for normal, single textures objects, this shouldn't be a problem. Are you sure that the position of the light in your non-working case is fine? Too far away and there won't be a shadow. The fact that it fest blocky in your example might be an indication for this. Try to lower the distance to see if that helps.
It's totally fine that additional visibility lists will be created when using the the ShadowHelper. The problem is when this happens every frame and that usually is the result of swallowing exceptions.
I'll take a look at your example, when i'm back home. I can't do much from here.

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #24 on: September 16, 2012, 10:08:52 pm »
I set both the light and the projector REALLY close just now to see whether I would get shadows and I still got none. I'm almost in the hero's face with the following code.

Code: [Select]
SimpleVector position = hero.get(0).getTransformedCenter();
position.z += 5f;
projector.setPosition(position);//hero.getRoot().getTransformedCenter()
projector.moveCamera(new SimpleVector(0, -1, 0), 3.75f);//200

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #25 on: September 17, 2012, 04:17:04 am »
Finally something to show for. I think it's a ShadowHelper bug because even this fraction of the shadow doesn't appear in most areas of the ground (the only receiver):


Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #26 on: September 22, 2012, 09:39:46 pm »
I tried your box test case, but i'm not sure what it's about to tell me other than that test case works?! That the shadow gets edgy is totally normal, because you are looking at it from a very close distance at a very low angle...every unfiltered texture would look that way in that scene and the shadows won't be filtered, so the result is to be expected.

About your screen shot..well, you have to keep in mind that a view frustum will be created for the shadow map just like the one for the camera. If you are moving pretty close to something, that frustum doesn't much of the geometry that is close. And i think that this is what you see in the screen shot. Everything else is just out of the frustum. Try to increase the fov (watch the stupid minFov and maxFov settings...) in the Projector and the result should change. If it does, try to move the projector out step by step to see what happens then.

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #27 on: September 22, 2012, 10:24:39 pm »
I'm not sure to which test you're referring, but it's nothing recent and it certainly wasn't done after I saw this fraction of a shadow. I'll play with FOV and report my findings, but I don't think that's it (in my opinion it's related to the complexity and size of the floor).
« Last Edit: September 22, 2012, 10:48:11 pm by AGP »

Offline EgonOlsen

  • Administrator
  • quad
  • *****
  • Posts: 11775
    • View Profile
    • http://www.jpct.net
Re: ShadowHelper Code Never Completes
« Reply #28 on: September 22, 2012, 10:33:41 pm »
I'll play with FOV and report my findings, but I don't think that's it (in my opinion it's related to the complexity and size of the floor).
The shadow mapping doesn't care about complexity as long as all parts (if there are more than one) have been added as receivers. It might help to render the floor as wireframe in addition to see the actual polygons.

Offline AGP

  • quad
  • ******
  • Posts: 1526
    • View Profile
Re: ShadowHelper Code Never Completes
« Reply #29 on: September 22, 2012, 10:47:13 pm »
Check this out. Setting minimum FOV to .02f helped, but didn't solve it. The further ahead I move, the less of the shadow there is. A few steps ahead of the second screen there's no shadow at all. But on another part of the ground (merged together with that one in Max), the shadow is complete again, until it disappears again. In total there are four parts of the floor, each behaving the same way.