5

How can I get a graph legend that automatically gets my maximum and minimum elevation from an object? Basically, I have an object that dinamically paints other with waves. I have a node setup that gives me color by relative height. I want to retrieve now the maximum and minimum absolute values of height that the waves are at current frame. I need to know how to build this http://lroc.sese.asu.edu/news/uploads/WAC_color_medium.4.colorbar.png that adapts height values from current time. this is my current situation: height_values the following blend file has my current code. Please fill free to change or adapt as you need/want to make it work.

MCunha
  • 1,263
  • 7
  • 12

1 Answers1

6

This script will generate a legend plane with text on the 2nd layer.

enter image description here

This script assumes the terrain object's name is "Plane". To change it, change the object's name in line 7:

obj = bpy.data.objects['Plane'] # Reference terrain object

The color ramp was generated according to the example in the question above (3 swatches with pure green, blue and red in that order). Anyway, the script takes the colors from the original terrain object's color ramp.

It also generates 7 rungs to the legend's ladder according to the provided example. To change this, find the rungs variable i the script and change its value.

import bpy
from math import radians

def update_legend( scene ):
    C = bpy.context

    ## Get global height coordinates of terrain vertices
    obj = bpy.data.objects['Plane'] # Reference terrain object
    m = obj.to_mesh( C.scene, True, 'RENDER' )

    offset  = 0.5 # The value placed in the math.add node
    heights = [ v.co.z + offset for v in m.vertices ]

    minZ = min( heights )
    maxZ = max( heights )

    ## Generate legend color bar

    legendPlane = None
    if 'legend' in bpy.data.objects:
        legendPlane = bpy.data.objects[ 'legend' ]
    else:
        # Add legened plane on layer 2
        bpy.ops.mesh.primitive_plane_add( layers = [ i == 1 for i in range(20) ] )

        legendPlane = bpy.data.objects[ C.object.name ]
        legendPlane.name = 'legend'
        legendPlane.dimensions.y = 20

        bpy.ops.object.transform_apply( scale = True ) # Apply scale (dim change)

        # Setup legend material
        cr = obj.active_material.node_tree.nodes['ColorRamp']

        legendMat = None
        if "legend" not in bpy.data.materials:
            legendMat = bpy.data.materials.new("legend")

            legendMat.use_nodes = True
            t = legendMat.node_tree

            t.nodes.remove( t.nodes['Diffuse BSDF'] )

            emit = t.nodes.new( 'ShaderNodeEmission' )
            mo   = t.nodes['Material Output']

            # Link Emission to Material Output
            t.links.new( emit.outputs['Emission'], mo.inputs['Surface'] )

            # Generate new color ramp and link to Emission node
            colorRamp = t.nodes.new( 'ShaderNodeValToRGB' )
            t.links.new( emit.inputs['Color'], colorRamp.outputs['Color'] )

            # Add another color swatch in the middle of the ramp
            colorRamp.color_ramp.elements.new( 0.5 )

            # Set color ramp colors
            originalRampColors = obj.active_material.node_tree.nodes['ColorRamp'].color_ramp.elements[:]
            for i, c in enumerate( originalRampColors ):
                color = c.color[:]
                colorRamp.color_ramp.elements[i].color = color

            # Add Texture coordiantes and Mapping nodes
            mapp  = t.nodes.new( 'ShaderNodeMapping'  )
            texco = t.nodes.new( 'ShaderNodeTexCoord' )

            # Set up texture coordiantes Y rotation
            mapp.rotation[1] = radians( 45 )

            # Set up links
            t.links.new( mapp.inputs['Vector'],   texco.outputs['Object'] )
            t.links.new( colorRamp.inputs['Fac'], mapp.outputs['Vector'] )

        else:
            legendMat = bpy.data.materials["legend"]

        legendPlane.active_material = legendMat

    ## Generate legend text

    legendTextMat = None
    if 'legendText' in bpy.data.materials:
        legendTextMat = bpy.data.materials["legendText"]
    else:
        # Generate a material for the text
        legendTextMat = bpy.data.materials.new( "legendText" )

        legendTextMat.use_nodes = True
        t = legendTextMat.node_tree
        if 'Diffuse BSDF' in t.nodes: 
            t.nodes.remove( t.nodes['Diffuse BSDF'] )

        emit = t.nodes.new( 'ShaderNodeEmission' )
        mo   = t.nodes['Material Output'] if 'Material Output' in t.nodes else t.nodes.new( 'ShaderNodeOutputMaterial' )
        t.links.new( emit.outputs['Emission'], mo.inputs['Surface'] )
        emit.inputs['Color'].default_value = (0,0,0,1) # Set color to black

    # Calculate text positions
    heightRange = maxZ - minZ
    rungs       = 7
    interval    = heightRange / rungs

    ladderText = [ str( round( minZ + i * interval - offset, 2 ) ) for i in range(2, rungs) ]
    ladderText = [ str( round( minZ - offset, 2 ) ) ] + ladderText + [ str( round( maxZ - offset, 2 ) ) ] # Add min and max Z values

    legendGlobCoo = [ legendPlane.matrix_world * v.co for v in legendPlane.data.vertices ]
    legendY = [ co.y for co in legendGlobCoo ]

    minY, maxY = ( min( legendY ), max( legendY ) )
    yInterval  = ( maxY - minY ) / rungs
    yPositions = [ minY + i * yInterval for i in range( rungs ) ]

    # Set X position as legend plane's maximum X + a space of 0.5 blender units
    xPos = max([ co.x for co in legendGlobCoo ]) + 0.5

    for i, yPos, text in zip( range(rungs), yPositions, ladderText ):
        name = "rung%s" % i

        o = None
        if name in bpy.data.objects:
            o = bpy.data.objects[ name ]
        else:
            bpy.ops.object.text_add( location = ( xPos, yPos, 0 ), layers = [ i == 1 for i in range(20) ] )
            o = bpy.data.objects[ bpy.context.object.name ]
            o.name = name
            o.location.y += i * ( o.dimensions.y / 2 )

        o.data.body = text

        o.active_material = legendTextMat

bpy.app.handlers.frame_change_pre.append( update_legend )

EDITED:

Fixed the issue that occurred when running this script with a modified mesh (in this case with dynamic paint), by converting the modified object to a mesh and using its coordinates. Here's an image of the legend generated at frame 110 of your simulation:

EDITED2:

Now updates values according to current frame via an app_handler.

EDITED3:

corrected line 128 > range(1,rungs) to range(2,rungs), so legend of scale bar is correct

TLousky
  • 16,043
  • 1
  • 40
  • 72
  • I gave it a try, but when I press play, it doesn't give me the max height of the plane for the current frame. It has the same problem that I have with my script, only updates the info, if I apply the dynamic paint modifier. I'll vote +1 because I like the way that instantly makes the scale bar but I will not accept as final answer. – MCunha Oct 27 '16 at 09:53
  • Haven't noticed the attached blendfile. Will try to fix it on your file and update if successfull. – TLousky Oct 27 '16 at 09:55
  • 1
    Fixed the problem @MCunha, it required recalculating the modified mesh with the object.to_mesh() method (line 9). – TLousky Oct 27 '16 at 11:00
  • I think you are almost there mate! If I press play with your script running, It really gets coordinates for that frame but doesn't update for other frames. I expect that on 1st frame max height and min height will be 0. After that, max height and min, should change when frame changes. If you see my script, in my blend file, it was supposed to recalculate/update text frame by frame. – MCunha Oct 27 '16 at 11:16
  • Now updates existing text with each execution. – TLousky Oct 27 '16 at 11:29
  • I'm sorry again. But it still doesn't updates text. Use my Blend file, put your script there, run it, and press play... In my file it doesn't update. I think it's need to erase data at the end of frame (after render), then redo at next frame before render (but i'm really a noob with python, just guessing). – MCunha Oct 27 '16 at 11:39
  • 1
    It doesn't update automatically, you need to re-run the script. To make it update automatically it should be possible to add this to an app handler (but that might not be a good idea). http://blender.stackexchange.com/questions/18463/get-actual-object-location-in-animation-handler – TLousky Oct 27 '16 at 11:42
  • do you know if it is possible to do an other script, that "says" to blender to run this script every time frame changes? – MCunha Oct 27 '16 at 11:44
  • Added to an app handler. Run the script once and it will update with every frame change. – TLousky Oct 27 '16 at 11:48
  • 1
    It works like a charm! thank you so much! You, now, are like a God to me! You don't know how I appreciate this! Many many thanks! – MCunha Oct 27 '16 at 11:50
  • 1
    Sure man :-D Nothing divine here, just some python :) – TLousky Oct 27 '16 at 11:52
  • Hello TLousky! Your script has a minor bug in the scale bar (but your's It's ok for my purposes): the maximum value of the scale bar, doesn't match the maximum height of the object, yet your variable "maxZ" does! there is something on the rung calculations (I don't know what) that makes the maximum value a little bit minor than the real maxZ. I think it would be great if you take a look, when you have time, and reedit your answer, so other users could enjoy this great script! I've noticed when I was exporting CSV files for analysis, then rechecked with object dimensions. – MCunha Oct 27 '16 at 17:07
  • Hello again! Corrected the bug, I've found out it at: line 128 > range(2,rungs) instead of range(1,rungs) – MCunha Oct 27 '16 at 17:18
  • Awesome, see you've edited the answer, cheers. – TLousky Oct 27 '16 at 21:43