Time for Action: Unique Object Labels

This section is divided into two parts. In the first part, you will develop code to generate a random scene with cones and cylinders. Each object will be assigned a unique object label that will be used for coloring the object in the offscreen renderbuffer. In the second part, we will configure the picker to work with unique labels. Let's get started:

  1. Open the ch08_02_picking-initial.html file in your browser. This is a scene that is only showing the floor object. We are going to create a scene that contains multiple objects that can be either balls or cylinders.
  2. Open ch08_02_picking-initial.html in a source code editor.
  3. We will write code so that each object in the scene can have the following:
    • A position assigned randomly
    • A unique object label color
    • A non-unique diffuse color
    • A scale factor that will determine the size of the object
  4. We have provided empty functions that you will implement in this section.
  5. Let's write the positionGenerator function. Scroll down to it and add the following code:
function positionGenerator() {
const
flagX = Math.floor(Math.random() * 10),
flagZ = Math.floor(Math.random() * 10);

let x = Math.floor(Math.random() * 60),
z = Math.floor(Math.random() * 60);

if (flagX >= 5) {
x = -x;
}
if (flagZ >= 5) {
z = -z;
}

return [x, 0, z];
}
  1. Here, we are using the Math.random function to generate the x and z coordinates for an object in the scene. Since Math.random always returns a positive number, we use the flagX and flagZ variables to randomly distribute the objects on the x-z plane (floor). Also, because we want all of the objects to be on the x-z plane, the y component is always set to 0 in the return statement.
  2. Let's write a unique object label generator function. Scroll to the empty objectLabelGenerator function and add the following code:
const colorset = {};

function objectLabelGenerator() {
const
color = [Math.random(), Math.random(), Math.random(), 1],
key = color.toString();

if (key in colorset) {
return objectLabelGenerator();
}
else {
colorset[key] = true;
return color;
}
}
  1. We create a random color using the Math.random function. If the key variable is already a property of the colorset object, then we call the objectLabelGenerator function recursively to get a new value; otherwise, we make key a property of colorset and then return the respective color. Notice how well the handling of JavaScript objects as sets allows us to resolve any possible key collision.
  2. Write the diffuseColorGenerator function. We will use this function to assign diffuse properties to the objects:
function diffuseColorGenerator(index) {
const color = (index % 30 / 60) + 0.2;
return [color, color, color, 1];
}
  1. This function represents the case where we want to generate colors that are not unique. The index parameter represents the index of the object in the scene.objects list to which we are assigning the diffuse color. In this function, we are creating a gray-level color since the r, g, and b components in the return statement all have the same color value.
  2. The diffuseColorGenerator function will create collisions every 30 indices. The remainder of the division of the index by 30 will create a loop in the sequence:
0 % 30 = 0
1 % 30 = 1
...
29 % 30 = 29
30 % 30 = 0
31 % 30 = 1
...
  1. Since this result is being divided by 60, the result will be a number in the [0, 0.5] range. Then, we add 0.2 to make sure that the minimum value that color has is 0.2. This way, the objects will not look too dark during the onscreen rendering (they would be black if the calculated diffuse color were 0).
  2. The last auxiliary function we will write is the scaleGenerator function:
function scaleGenerator() {
const scale = Math.random() + 0.3;
return [scale, scale, scale];
}
  1. This function will allow us to have objects of different sizes. 0.3 is added to control the minimum scaling factor that any object will have in the scene.
  2. Let's load 100 objects to our scene. By the end of this section, you will be able to test picking on any of them!
  3. Go to the load function and edit it so that it looks like this:
function load() {
scene.add(new Floor(80, 20));

for (let i = 0; i < 100; i++) {
const objectType = Math.floor(Math.random() * 2);

const options = {
position: positionGenerator(),
scale: scaleGenerator(),
diffuse: diffuseColorGenerator(i),
pcolor: objectLabelGenerator()
};

switch (objectType) {
case 1:
return scene.load('/common/models/ch8/sphere.json',
`ball_${i}`, options);
case 0:
return scene.load('/common/models/ch8/cylinder.json',
`cylinder_${i}`, options);
}
}
}
  1. The picking color is represented by the pcolor attribute. This attribute is passed in a list of attributes to the scene.load function. Once the object is loaded (using JSON/Ajax), load uses this list of attributes and adds them as object properties.
  2. The shaders in this exercise have already been set up for you. The pcolor property that corresponds to the unique object label is mapped to the uPickingColor uniform, and the uOffscreen uniform determines whether it is used in the fragment shader:
uniform vec4 uPickingColor;

void main(void) {

if (uOffscreen) {
fragColor = uPickingColor;
return;
}
else {
// on-screen rendering
}

}
  1. As described previously, we keep the offscreen and onscreen buffers in sync by using the render function as follows:
function render() {
// Off-screen rendering
gl.bindFramebuffer(gl.FRAMEBUFFER, picker.framebuffer);
gl.uniform1i(program.uOffscreen, true);
draw();

// On-screen rendering
gl.uniform1i(program.uOffscreen, showPickingImage);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
draw();
}
  1. Save your work as ch08_03_picking-no-picker.html.
  2. Open ch08_03_picking-no-picker.html in your browser.
  3. Click on Show Picking Image. What happens?
  4. The scene is being rendered to both the offscreen and default onscreen framebuffer. However, we have not configured the Picker callbacks yet.
  5. Open ch08_03_picking-no-picker.html in your source code editor.
  6. Scroll down to the configure function. The picker is already set up for you:
picker = new Picker(canvas, {
hitPropertyCallback: hitProperty,
addHitCallback: addHit,
removeHitCallback: removeHit,
processHitsCallback: processHits,
moveCallback: movePickedObjects
});
  1. This code fragment maps functions in the web page to picker callback hooks. These callbacks are invoked according to the picking state.
  2. We will now implement the necessary callbacks. Again, we have provided empty functions that you will need to code.
  3. Let's create the hitProperty function. Scroll down to the empty hitProperty function and add the following code:
function hitProperty(obj) {
return obj.pcolor;
}
  1. We are returning the pcolor property to make the comparison with the color that will be read from the offscreen framebuffer. If these colors match, then we have a hit.
  1. Write the addHit and removeHit functions. We want to create the effect where the diffuse color is changed to the picking color during picking. We need an extra property to temporarily save the original diffuse color so that we can restore it later:
function addHit(obj) {
obj.previous = obj.diffuse.slice(0);
obj.diffuse = obj.pcolor;
}
  1. The addHit function stores the current diffuse color in an auxiliary property named previous. Then, it changes the diffuse color to pcolor, the object-picking label:
function removeHit(obj) {
obj.diffuse = obj.previous.slice(0);
}
  1. The removeHit function restores the diffuse color.
  2. Now, let's write the code for processHits:
function processHits(hits) {
hits.forEach(hit => hit.diffuse = hit.previous);
}
  1. Remember that processHits is called upon exiting picking mode. This function will receive one parameter: the hits that the picker detected. Each element of the hits list is an object in scene. In this case, we want to give the hits their diffuse color back. For that, we use the previous property that we set in the addHit function.
  2. The last picker callback we need to implement is the movePickedObjects function:
function movePickedObjects(dx, dy) {
const hits = picker.getHits();

if (!hits) return;

const factor = Math.max(
Math.max(camera.position[0], camera.position[1]),
camera.position[2]
) / 2000;

hits.forEach(hit => {
const scaleX = vec3.create();
const scaleY = vec3.create();

if (controls.alt) {
vec3.scale(scaleY, camera.normal, dy * factor);
}
else {
vec3.scale(scaleY, camera.up, -dy * factor);
vec3.scale(scaleX, camera.right, dx * factor);
}

vec3.add(hit.position, hit.position, scaleY);
vec3.add(hit.position, hit.position, scaleX);
});
}
  1. This function allows us to move the objects in the hits list interactively. The parameters that this callback function receives are as follows:
    • dx: Displacement in the horizontal direction obtained from the mouse when it is dragged on canvas
    • dy: Displacement in the vertical direction obtained from the mouse when it is dragged on canvas
  2. Let's analyze the code. First, we retrieve all of the hits from the picker instance:
const hits = picker.getHits();
  1. If there are no hits, the function returns immediately:
if (!hits) return;
  1. We calculate a weighing factor that we will use later (the fudge factor):
const factor = Math.max(
Math.max(camera.position[0], camera.position[1]), camera.position[2]
) / 2000;
  1. We create a loop to go through the hits list so that we can update each object's position:
hits.forEach(hit => {
const scaleX = vec3.create();
const scaleY = vec3.create();

// ...
});
  1. The scaleX and scaleY variables are initialized for every hit.
  2. The Alt key is being used to perform dollying (moving the camera along its normal path). In this case, we want to move the objects that are in the picking list along the camera's normal direction when the user is pressing the Alt key to provide a consistent user experience.
  3. To move the hits along the camera normal, we use the dy (up-down) displacement, as follows:
if (controls.alt) {
vec3.scale(scaleY, camera.normal, dy * factor);
}
  1. This creates a scaled version of camera.normal and stores it into the scaleY variable. Notice that vec3.scale is an operation that's available in the glMatrix library.
  2. If the user is not pressing the Alt key, then we use dx (left-right) and dy (up-down) to move the hits in the camera plane. Here, we use the camera's up and right vectors to calculate the scaleX and scaleY parameters:
else {
vec3.scale(scaleY, camera.up, -dy * factor);
vec3.scale(scaleX, camera.right, dx * factor);
}
  1. We update the position of the hit:
vec3.add(hit.position, hit.position, scaleY);
vec3.add(hit.position, hit.position, scaleX);
  1. Save the page as ch08_04_picking-final.html and open it using your browser.
  1. You will see a scene like the one shown in the following screenshot:

  1. Click on Reset Scene several times and verify that you get a new scene every time.
  1. In this scene, all of the objects have very similar colors. However, each one has a unique picking color. To verify this, click on the Show Picking Image button. You will see on the screen what is being rendered in the offscreen buffer:

  1. Let's validate the changes that we made to the picker callbacks. Let's start by picking one object. As you can see, the object diffuse color becomes its picking color (this was the change you implemented in the addHit function):

  1. When the mouse is released, the object goes back to the original color. This is the change that was implemented in the processHits function.
  2. While the mouse button is held down over an object, you can drag it around. When this is done, movePickedObjects is being invoked.
  3. Pressing the Shift key while objects are being selected will tell the picker not to exit picking mode. This way, you can select and move more than one object at once:

  1. You will exit picking mode if you select an object and the Shift key is no longer pressed or if your next click does not produce any hits (in other words, clicking anywhere else).
  2. If you have any problems with this exercise or missed one of the steps, we have included the complete exercise in the ch08_03_picking-no-picker.html and ch08_04_picking-final.html files.

What just happened?

We have done the following:

  • Created the property-picking color. This property is unique for every object in the scene and allows us to implement picking based on it.
  • Modified the fragment shader to use the picking color property by including a new uniform, uPickingColor, and mapping this uniform to the pcolor object property.
  • Learned about the different picking states. We have also learned how to modify the Picker callbacks to perform specific application logic such as removing picked objects from the scene.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.38.125