Procedural geometry shading

I’m having a bit of trouble getting shading from procedural geometry to work. I am fairly certain that I am setting the normal vectors correctly, but I keep getting flat shading for the added VRObject. The VRBox environment seems to be working correctly, however. Is it something with how I am attaching them to self.render? Lots of code below:

from panda3d.core import loadPrcFileData, Vec2D, rad2Deg, Mat4, Vec3, AmbientLight, DirectionalLight, VBase4, deg2Rad, NodePath, Material
loadPrcFileData("", """#side-by-side-stereo 1
#undecorated 1
win-origin 15 15
win-size 640 480""")

import math
from VRBox import VRBox
from VRObjects import VRObject

from direct.showbase.ShowBase import ShowBase

class World(ShowBase):
	def __init__(self):
		ShowBase.__init__(self)
		scnsz = Vec2D(0.31,0.2325)
		scndist = 0.52
		wkspsz = scnsz
		wkspdp = 2*0.12
		IOD = 0.3
		Desired_CD = scndist
		self.disableMouse()
		self.setBackgroundColor(.5,0,0)	
		
		# Mirror the display (vertical) for task room & correct aspect ratio for stereo
		#self.setAspectRatio(scnsz.x/(2*scnsz.y))
		self.win.setInverted(True)
		
		# Position the camera
		self.camera.setPos(0,0,1*scndist)
		self.camera.setHpr(0,-90,0)
		
		# Setup the camera lens
		self.camLens.setNear(0.001)
		self.camLens.setFar(100)
		Xang = rad2Deg(2.*math.atan(scnsz.x/(2.*scndist)))
		Yang = rad2Deg(2.*math.atan(scnsz.y/(2.*scndist)))	
		CD_Hack = Desired_CD*math.tan(deg2Rad(Xang)/2)/(IOD*2)
		self.camLens.setFov(Xang,Yang)
		self.camLens.setInterocularDistance(IOD)
		self.camLens.setConvergenceDistance(CD_Hack)
		
		# Add the VRBox
		self.box = VRBox().generate(Vec3(0,0,-.06),wkspsz.x,wkspsz.y,wkspdp/2)
		
		# Add checker pattern to the box
		checkers = loader.loadTexture("textures/checker.bmp")
		checkers.setMagfilter(checkers.FTNearest)
		self.box.setTexture(checkers)
		self.boxmat = Material()
		self.boxmat.setShininess(6.)
		self.box.setMaterial(self.boxmat)
		self.box.reparentTo(self.render)
		
		# Add a directional light
		dlight = DirectionalLight('dlight')
		dlight.setColor(VBase4(0.8, 0.8, 0.8, 1))
		dlnp = self.render.attachNewNode(dlight)
		dlnp.lookAt(1,-1,-1)
		self.render.setLight(dlnp)
		
		# Add an ambient light
		alight = AmbientLight('alight')
		alight.setColor(VBase4(0.3, 0.3, 0.3, 1))
		alnp = self.render.attachNewNode(alight)
		self.render.setLight(alnp)
		
		# Add a cursor
		self.cursor	= VRObject('cylinder', scnsz.y, Vec3(0.1,0.1,0.2))
		self.cursor.getNodePath().reparentTo(self.render)
		self.cursor.getNodePath().setHpr(0,110,0)
		
W = World()
W.run()
from pandac.PandaModules import GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomTriangles, Geom, GeomNode, NodePath, GeomPoints, Vec3, Vec2

class VRBox:
	def generate(self,cent,w,h,d):
		format = GeomVertexFormat.getV3n3t2()
		boxData = GeomVertexData("box", format, Geom.UHStatic)
		vertexWriter = GeomVertexWriter(boxData,'vertex')
		normWriter = GeomVertexWriter(boxData,'normal')
		uvWriter = GeomVertexWriter(boxData,'texcoord')
		d2 = d/w
		coords = [Vec3(w, h, -d), Vec3(-w, h, -d), Vec3(-w, -h, -d), Vec3(w, -h, -d), # Back
			Vec3(-w, h, -d), Vec3(-w, h, d), Vec3(-w, -h, d), Vec3(-w, -h, -d),       # Left
			Vec3(w, h, d), Vec3(w, h, -d), Vec3(w,-h,-d), Vec3(w, -h, d),             # Right
			Vec3(-w, h, -d), Vec3(w, h, -d), Vec3(w, h, d), Vec3(-w, h, d),           # Top
			Vec3(w, -h, -d), Vec3(-w, -h, -d), Vec3(-w, -h, d), Vec3(w, -h, d)]       # Bottom
		norms = [Vec3(0, 0, 1), Vec3(0, 0, 1), Vec3(0, 0, 1), Vec3(0, 0, 1), # Back
			Vec3(1, 0, 0), Vec3(1, 0, 0), Vec3(1, 0, 0), Vec3(1, 0, 0),      # Left
			Vec3(-1, 0, 0), Vec3(-1, 0, 0), Vec3(-1, 0, 0), Vec3(-1, 0, 0),  # Right
			Vec3(0, -1, 0), Vec3(0, -1, 0), Vec3(0, -1, 0), Vec3(0, -1, 0),  # Top
			Vec3(0, 1, 0), Vec3(0, 1, 0), Vec3(0, 1, 0), Vec3(0, 1, 0)]      # Bottom
		texts = [Vec2(1, 1), Vec2(0, 1), Vec2(0, 0), Vec2(1, 0), # Back
			Vec2(0, 0), Vec2(0, d2), Vec2(1, d2), Vec2(1, 0),    # Left
			Vec2(0, d2), Vec2(0, 0), Vec2(1, 0), Vec2(1, d2),    # Right
			Vec2(0, 0), Vec2(1, 0), Vec2(1, d2), Vec2(0, d2),    # Top
			Vec2(1, 0), Vec2(0, 0), Vec2(0, d2), Vec2(1, d2)]    # Bottom
		
		for pos, norm, tex in zip(coords, norms, texts):
			vertexWriter.addData3f((pos/2)+cent)
			normWriter.addData3f(norm)
			uvWriter.addData2f(tex)
		
		triangles = GeomTriangles(Geom.UHStatic)
		
		def addQuad(v0, v1, v2, v3):
			triangles.addVertices(v0, v1, v2)
			triangles.addVertices(v0, v2, v3)
			triangles.closePrimitive()
			
		for i in range(0,5):
			addQuad(i*4,i*4+1,i*4+2,i*4+3)
		triangles.closePrimitive()
		geom = Geom(boxData)
		geom.addPrimitive(triangles)
		
		node = GeomNode("checkerbox")
		node.addGeom(geom)
		
		return NodePath(node)
from pandac.PandaModules import GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomTriangles, Geom, GeomNode, NodePath, GeomPoints, Vec3, Material, VBase4

from math import sin, cos, pi, fmod

class VRObject:
	def __init__(self,type='sphere',wrldsz=0.2325, sz=Vec3(0.1,0.1,0.1)):
		self.DEF_LATS = 30
		self.DEF_LONGS = 30
		self.wrldsz = wrldsz
		self.sz = sz
		self.type = type
		self.material = Material()
		self.material.setShininess(0)
		self.material.setAmbient(VBase4(0, 1, 0, 1))
		self.material.setDiffuse(VBase4(0, 0, 1, 1))
		self.material.setSpecular(VBase4(1, 1, 0, 1))
		self.material.setEmission(VBase4(0, 0, 0, 1))
		
		
		self.rootNodePath = NodePath('object')
		format = GeomVertexFormat.getV3n3()
		self.objectData = GeomVertexData('object', format, Geom.UHStatic)
		self.vertex = GeomVertexWriter(self.objectData,'vertex')
		self.normals = GeomVertexWriter(self.objectData,'normal')
		self.triangles = GeomTriangles(Geom.UHStatic)
		self.createandaddcoords(type)
		self.geom = Geom(self.objectData)
		self.geom.addPrimitive(self.triangles)
		self.geomnode = GeomNode('object')
		self.geomnode.addGeom(self.geom)
		NodePath(self.geomnode).reparentTo(self.rootNodePath)
		self.rootNodePath.setMaterial(self.material)
		self.rootNodePath.setScale(self.sz*self.wrldsz)
		
	def getNodePath(self):
		return self.rootNodePath
		
	def setColor(self,R,G,B):
		self.material.setAmbient(VBase4(R,G,B,1))
		self.material.setDiffuse(VBase4(R,G,B,1))
		
	def createandaddcoords(self,type):
		if type=='sphere':
			coords = [None]*(self.DEF_LATS*self.DEF_LONGS+2)
			coords[0] = Vec3(0,0,-1)
			for i in range(0,self.DEF_LATS):				
				vertang = pi*(i+1)/(self.DEF_LATS+1)-pi/2
				z = sin(vertang)
				hzrad = cos(vertang)
				for j in range(0,self.DEF_LONGS):
					idx = i*self.DEF_LONGS+j+1
					hrzang = 2*pi*j/self.DEF_LONGS
					x = hzrad*cos(hrzang)
					y = hzrad*sin(hrzang)
					coords[idx] = Vec3(x,y,z)
			endidx = self.DEF_LATS*self.DEF_LONGS+1
			coords[endidx] = Vec3(0,0,1)
			norms = coords
		elif type=='cylinder':
			coords = [None]*(self.DEF_LONGS*4+2)
			norms = [None]*(self.DEF_LONGS*4+2)
			coords[0] = Vec3(0,0,-1)
			norms[0] = Vec3(0,0,-1)
			for i in range(0, self.DEF_LONGS):
				for j in range(0,4):
					idx = i+self.DEF_LONGS*j+1
					x = cos(2*pi*i/self.DEF_LONGS)
					y = sin(2*pi*i/self.DEF_LONGS)
					norms[idx] = Vec3(x,y,0)
					if j<2:
						coords[idx] = Vec3(x,y,-1)
					else:
						coords[idx] = Vec3(x,y,1)
					if j==0:
						norms[idx] = Vec3(0,0,-1)
					elif j==3:
						norms[idx] = Vec3(0,0,1)
			endidx = self.DEF_LONGS*4+1
			coords[endidx] = Vec3(0,0,1)
			norms[endidx] = Vec3(0,0,1)
			
		for pos, norm in zip(coords, norms):
			self.vertex.addData3f(pos)
			self.normals.addData3f(norm.normalize())
			
		if type=='sphere':
			self.addCap('bottom')
			for i in range(1,self.DEF_LATS):
				self.addLatitudes(i)
			self.addCap('top')
		elif type=='cylinder':
			self.addCylEnd('bottom')
			self.addLatitudes(2)
			self.addCylEnd('top')
		
	def addCap(self,whatend):
		if whatend=='bottom':
			for i in range(0,self.DEF_LONGS):
				idx1 = i+1
				idx2 = int(fmod(i+1,self.DEF_LONGS)+1)
				self.triangles.addVertices(0,idx2,idx1)
				self.triangles.closePrimitive()
		elif whatend=='top':
			endidx = self.DEF_LATS*self.DEF_LONGS+1
			startspot = 1+self.DEF_LONGS*(self.DEF_LATS-1)
			for i in range(0,self.DEF_LONGS):
				idx1 = i+startspot
				idx2 = int(fmod(i+1,self.DEF_LONGS)+startspot)
				self.triangles.addVertices(endidx,idx1,idx2)
				self.triangles.closePrimitive()
				
	def addLatitudes(self,level):
		strtpt1 = 1+(level-1)*self.DEF_LONGS
		strtpt2 = 1+level*self.DEF_LONGS
		for i in range(0,self.DEF_LONGS):
			nextpt1 = int(fmod(i+1,self.DEF_LONGS)+strtpt1)
			nextpt2 = int(fmod(i+1,self.DEF_LONGS)+strtpt2)
			self.addQuad(i+strtpt1,nextpt1,nextpt2,i+strtpt2)
		
	def addCylEnd(self,whatend):
		if whatend=='bottom':
			for i in range(0,self.DEF_LONGS):
				idx1 = i+1
				idx2 = int(fmod(i+1,self.DEF_LONGS)+1)
				self.triangles.addVertices(0,idx2,idx1)
				self.triangles.closePrimitive()
		elif whatend=='top':
			for i in range(0,self.DEF_LONGS):
				idx1 = i+1+self.DEF_LONGS*3
				idx2 = int(fmod(i+1,self.DEF_LONGS)+1+self.DEF_LONGS*3)
				self.triangles.addVertices(self.DEF_LONGS*4+1,idx1,idx2)
				self.triangles.closePrimitive()

	def addQuad(self, v0, v1, v2, v3):
		self.triangles.addVertices(v0, v1, v2)
		self.triangles.closePrimitive()
		self.triangles.addVertices(v0, v2, v3)
		self.triangles.closePrimitive()
		
	def changeSize(self, newsize):
		self.sz = newsize
		self.rootNodePath.setScale(self.sz*self.wrldsz)

You seem to be using polygon normals, which result in flat shading. (In your code, the normal vector is the same for all vertices in a polygon). You should be using vertex normals instead; ie. the vertex is actually shared between different polygons, and the normal is pointing away from each corner.

On the sphere object, I am pretty sure that I am using vertex normals. I set the normal for each vertex to be the same as the position coordinate for the unit sphere (centered at the origin) itself. I guess the result I am seeing is not the same as the flat shading shown in this image https://upload.wikimedia.org/wikipedia/commons/8/84/Phong-shading-sample.jpg, but rather just a circle completely filled with the material’s ambient color.

Oh, you’re not talking about your VRBox class, which clearly uses polygon normals? Well, before we can judge what’s wrong with the normal generation of VRObject, we’d have to see its source code.

Sorry for the confusion. The VRObject code should be the third set of code above.

norms.normalize() normalizes the vector in-place, and returns a boolean value indicating whether or not the vector could be normalized (ie. is not a zero-length vector). This True value gets cast to an integer of value 1 when passing to addData3, which expands out to (1, 0, 0), hence all vertices will have that normal.

The corrected code is:

      for pos, norm in zip(coords, norms):
         norm.normalize()
         self.vertex.addData3f(pos)
         self.normals.addData3f(norm)

Awesome! You’re the best! :smiley: