Adding lights to procedural mesh, adding a glossmap to procedural mesh

Hey,

im currently trying to get into procedural mesh generation. So Far I implemented the Panda3D viewer into Tkinter and I am generating a plane-mesh with height offset from perlin-noise. My ultimate Goal is to programm some tool to generate normal/height maps and gloss maps from Textures (with sliders and stuff). To better see specular highlights I would like to add lights to the scene, but except the ambient light nothing seems to work at all. I tried adding for example the pointlight via

plight = PointLight(‘plight’)
plight.setColor(VBase4(0.2, 0.2, 0.2, 1))
plnp = render.attachNewNode(plight)
plnp.setPos(10, 20, 0)
render.setLight(plnp)

but the mesh stays black (i also tried altering the light or even letting it rotate around in the scene, nothing worked). Am I forgetting something in my mesh (normals for example)? I assumed the normals got generated automatically since the view of the mesh seems about right.

Also I’d like to add the shader maps ofcourse (first just the ones that come with Panda3D, im not familiar with GLSL or any shader language). The Texture gets applied currectly (in this case just the x-gradient of the noise, which makes it look a bit like a “fake shadow cast”). But as soon as I change the TextureStage to use TextureStage.MGloss(line 111), the Texture vanishes, no error is raised and there is no visible gloss on the mesh.

here my code (its a bit messy, sorry in advance for the comments):

from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
import tkinter
import numpy as np
import random

class app(ShowBase):
	def __init__(self,*args,**kwargs):
		ShowBase.__init__(self,*args,**kwargs)
		self.width = 1600
		self.height = 900
		self.resolution = 2**8


		self.startTk()
		self.root = self.tkRoot
		self.root.update()
		self.root.geometry('{0}x{1}'.format(self.width,self.height))
		self.root.columnconfigure(0,weight=1)
		self.root.rowconfigure(0,weight=1)
		self.canvas_frame = tkinter.LabelFrame(self.root,text='Panda3d canvas')
		self.canvas_frame.grid(row=0,column=0,sticky=tkinter.N+tkinter.S+tkinter.W+tkinter.E)
		self.menu_frame = tkinter.LabelFrame(self.root,text='Menu')
		self.menu_frame.grid(row=0,column=1,sticky='news')
		self.button1 = tkinter.Button(self.menu_frame,text='button',command = self.change_mesh)
		self.button1.pack()
		self.canvas = tkinter.Canvas(self.canvas_frame)
		self.canvas.pack(fill=tkinter.BOTH,expand=True)
		self.canvas.columnconfigure(0,weight=1)
		self.canvas.rowconfigure(0,weight=1)
		self.canvas_id = self.canvas.winfo_id()
		self.root.update()
		self.props = WindowProperties()
		self.props.set_parent_window(self.canvas_id)
		self.props.setOrigin(0, 0)
		self.props.setSize(self.width, self.height)
		self.makeDefaultPipe()
		self.openDefaultWindow(props=self.props)

		self.format = GeomVertexFormat.getV3c4t2() # vertex + color + texcoord
		self.init_mesh()
		self.disableMouse()
		self.camera.setHpr(0, -35, 0 )
		self.camera.setPos(0,-40, 25)

		self.setBackgroundColor(.2,.2,.2,)
		#Add the spinCameraTask procedure to the task manager.
		# self. id = self.taskMgr.add(self.spinCameraTask, "SpinCameraTask")
		self.start_mainloop()

	# Define a procedure to move the camera.
	def spinCameraTask(self, task):
		angleDegrees = task.time * 6.0
		angleRadians = angleDegrees * (np.pi / 180.0)
		self.camera.setPos(50 * np.sin(angleRadians), -50.0 * np.cos(angleRadians), 25)
		self.camera.setHpr(angleDegrees, -25, 0 )
		# self.taskMgr.remove(self.id)
		return task.cont

	def init_mesh(self):
		'generates a vertexprimitive from a perlin noise field and adds its node to the scene'
		self.vdata =  GeomVertexData('Triangle Mesh', self.format, Geom.UHStatic)
		self.vertex = GeomVertexWriter(self.vdata, 'vertex')
		self.color = GeomVertexWriter(self.vdata, 'color')
		self.texcoord = GeomVertexWriter(self.vdata, 'texcoord')

		self.heightmap,lin,n = self.sum_perlin(resolution = self.resolution,seed_nr = random.randint(0,1000))
		self.gradientmap = np.gradient(self.heightmap)
		self.tex = Texture()
		self.tex.setup2dTexture(self.resolution,self.resolution, Texture.T_unsigned_byte, Texture.F_luminance)
		self.gradientmap = self.gradientmap - np.amin(self.gradientmap)
		self.buff = self.gradientmap[0]*((self.resolution-1)/np.amax(self.gradientmap[0]))
		self.buff = (self.resolution-1) - self.buff
		self.buff = self.buff.astype(np.uint8).tostring()
		self.tex.setRamImage(self.buff)

		self.vertices = []
		offset = 10
		for column,x in zip(self.heightmap,lin):
			for value,y in zip(column,lin):
				self.vertices.append([x-offset,y-offset,value])

		color_offset = np.amin(self.heightmap)
		scale = 1/np.amax(self.heightmap-color_offset)
		# color_offset = np.amin(self.gradientmap)
		# scale = 1/np.amax(self.gradientmap - color_offset)
		ii = 0
		kk = 0
		for vertex in self.vertices:
			self.vertex.addData3f(vertex[1], vertex[0], vertex[2])
			self.texcoord.addData2f(ii/(self.resolution-1),kk/(self.resolution-1))
			ii = ii + 1
			if ii == self.resolution:
				ii = 0
				kk = kk + 1
			self.color.addData4f(scale*(vertex[2]-color_offset),scale*(vertex[2]-color_offset),scale*(vertex[2]-color_offset),1)
			# self.color.addData4f(scale*(color-color_offset),scale*(color-color_offset),scale*(color-color_offset),1)
		self.prim = GeomTriangles(Geom.UHStatic)
		n = len(lin)
		self.prim.addVertices(0,1,n+1)
		for ii in range(1,n*(n-1)):
			if (ii)%n == 0:
				self.prim.addVertices(ii,ii+1,ii+n)
			elif (ii+1)%n == 0:
				self.prim.addVertices(ii,ii+n,ii+n-1)
			else:
				self.prim.addVertices(ii,ii+1,ii+n)
				self.prim.addVertices(ii,ii+n,ii+n-1)
		self.prim.close_primitive()
		self.gloss_ts = TextureStage('ts')
		# self.gloss_ts.setMode(TextureStage.MGloss)
		self.geom = Geom(self.vdata)
		self.geom.addPrimitive(self.prim)
		self.node = GeomNode('gnode')
		self.node.addGeom(self.geom)
		self.nodepath = render.attachNewNode(self.node)
		self.nodepath.setTexture(self.gloss_ts,self.tex)
		self.nodepath.reparentTo(self.render)
		self.nodepath.setShaderAuto()


	def change_mesh(self):
		'generates new mesh with perlin noise (new seed)'
		self.nodepath.removeNode()
		self.init_mesh()

	def sum_perlin(self,number_of_sums = 2, weights = [3,0.5],noise_sizes = [3,20], seed_nr = 2,resolution=2**8):
		'sums different perlin noises to generate a more interesting mesh with more details'
		if len(weights) != number_of_sums:
			print('number of sums need to equal length of weights')
			return

		lin = np.linspace(0,noise_sizes[0],resolution,endpoint=False)
		n = len(lin)
		x,y = np.meshgrid(lin,lin)
		sum_perlin = weights[0] * self.perlin(x,y,seed=seed_nr)
		ii = 1
		while ii < number_of_sums:
			lin = np.linspace(0,noise_sizes[ii],resolution,endpoint=False)
			x,y = np.meshgrid(lin,lin)
			sum_perlin = sum_perlin + (weights[ii] * self.perlin(x,y,seed=seed_nr))
			ii = ii + 1
		return sum_perlin, lin, n

	def start_mainloop(self):
		'starts mainloop and hands it over to TK backend'
		self.spawnTkLoop()
		self.tkRun()

	def perlin(self,x,y,seed=0):
		'generates perlin noise'
		# permutation table
		np.random.seed(seed)
		p = np.arange(self.resolution,dtype=int)
		np.random.shuffle(p)
		p = np.stack([p,p]).flatten()
		# coordinates of the top-left
		xi = x.astype(int)
		yi = y.astype(int)
		# internal coordinates
		xf = x - xi
		yf = y - yi
		# fade factors
		u = self.fade(xf)
		v = self.fade(yf)
		# noise components
		n00 = self.gradient(p[p[xi]+yi],xf,yf)
		n01 = self.gradient(p[p[xi]+yi+1],xf,yf-1)
		n11 = self.gradient(p[p[xi+1]+yi+1],xf-1,yf-1)
		n10 = self.gradient(p[p[xi+1]+yi],xf-1,yf)
		# combine noises
		x1 = self.lerp(n00,n10,u)
		x2 = self.lerp(n01,n11,u) # FIX1: I was using n10 instead of n01
		return self.lerp(x1,x2,v) # FIX2: I also had to reverse x1 and x2 here


	@staticmethod
	def lerp(a,b,x):
	    "linear interpolation"
	    return a + x * (b-a)

	@staticmethod
	def fade(t):
	    "6t^5 - 15t^4 + 10t^3"
	    return 6 * t**5 - 15 * t**4 + 10 * t**3

	@staticmethod
	def gradient(h,x,y):
	    "grad converts h to the right gradient vector and return the dot product with (x,y)"
	    vectors = np.array([[0,1],[0,-1],[1,0],[-1,0]])
	    g = vectors[h%4]
	    return g[:,:,0] * x + g[:,:,1] * y


if __name__ == "__main__":
	application = app(windowType='none')

Sorry, you’ll need to generate normals, which are essential for lighting calculations.

@rdb Alright, i already thought that would be the Problem. How would i add the normals? Im a bit confused about that, since the vertexdata expects an normal entry for every vertex, but the normal should correspond to the Vertexprimitives and not a vertex. How Are those linked?

For Example, if I have two GeomTriangles With two common Vertex, then the GeomVertexData expects me to fill in 4 normals, but obviously two triangles in 3 dimensional space only have two normals.

To explain–if I have it correctly: In general, the polygons of a 3D model are just an approximation of a possibly-more-complex surface–think of a polygonal representation of a round ball. Having one normal per vertex then allows us to shade between them, thus giving the impression of a smooth surface, rather than an angular one.

(And angular surfaces can still be represented in this system; I think that this is generally done by duplicating vertices–but I may be mistaken in that, and stand to be corrected!)

I suppose that one could come up with a system in which one shaded between triangle-normals–but with only one normal per triangle, I suspect that there’s less information stored, and thus less flexibility in the shading of the surface.

Yes, it depends on whether you want smooth surfaces (ie. the edges between the polygons to blend together) or whether you want flat triangles, ie. each polygon has its own normals, which makes the seam between the polygons obvious.

The way you calculate a triangle normal is (assuming A, B and C are the position of the vertices) by calculating the cross product of B - A and C - A and normalizing that (make sure you do it in the right order or it will be flipped inside-out). The way you calculate a vertex normal is by averaging the triangle normal for the surrounding triangles (and normalizing the result).

If you want flat normals, usually the thing to do is to duplicate the vertices for each primitive. (Don’t worry too much about added cost here; the GPU would end up duplicating the vertices anyway as part of the normal shading pipeline.)

Thank you Both for the detailed feedback. After thinking a bit more about the normals and reading Thaumaturge’s comment, it makes a Lot more sense to have the normals at the vertex Positions, if a vertex is a (exact) point on a surface, while all the points inbetween are just planar interpolations. Since im already calculating the gradient in my program for other purposes and since its only a discrete Function of two variables, I can also calculate the normals at the vertex Positions via the gradient. So its even quite easy and not too mind boggling to Match the normals to the correct vertices (I made up my own algorythm to find the corresponding vertices for every triangle, so it would have might been Hard to apply some premade algorythms for that)

okay. now that there is glossyness on my mesh, the next problem occured. The glossmap seems to be ignored and the whole mesh is glossy. When I comment the TextureStage.setMode out, the texture shows clear black spots where it should be glossy and nothing where it shouldnt.

nevermind, now its glossy where it should be (i had a luminance texture, no alpha texture and furthermore there seems to be kind of an error in the documentation, which says the map needs to range from 0 to 1 but it needs to range from for example 0 to 255 if you choose to give it unsigned integers) but still I thought I could apply a value inbetween 0 and 255 andn ot just 0 for no specularity and 255 for full specularity. Is this possible and im juts doing something wrong?

again, nevermind. Thanks for your help, everything works fine now. I just should sleep more