# -*- test-case-name: mv3d.test.test_mesh -*- # Copyright (C) 2007-2012 Mortal Coil Games # See LICENSE for details. """ """ from time import time from array import array from twisted.internet import defer from mv3d.net.pb import Cacheable, withClientUpdate from mv3d.net.security import Securable from mv3d.util.math3d import Vector, Quaternion from mv3d.util.noise import PerlinNoise from mv3d.phys.body import BodyWithColliders, IVisualObject, IVisualObjectView from mv3d.util.classgen import ClassGenerator from mv3d.util.persist import Persistable, UntypedReference, FloatVector, \ IDTuple, List, Float, Boolean, Pickled, IntVector, MapAttribute, Text, \ convertAutoSave from twisted.internet.defer import maybeDeferred, gatherResults from mv3d.phys.scenery import Grass from mv3d.util.persist import autoStore from mv3d.phys.iphys import IMeshGenCollider class MeshError(Exception): """ An error that occured with the mesh """ def frange(start, stop, n): nm1 = n - 1 nm1inv = 1.0 / nm1 return [nm1inv * (start * (nm1 - i) + stop * i) for i in range(n)] class IHeightGenerator: """ This can be used to generate heights for a height field """ def generate(self, pos): """ Generate a height at pos (x,z) Returns a float """ class NoiseHeightGenerator(IHeightGenerator, Cacheable, Securable, Persistable): """ Generate heights via Perlin Noise """ getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor stoppedObserving = Cacheable.stoppedObserving setCopyableState = Cacheable.setCopyableState multiplier = FloatVector(default=(4.0, 100.0, 4.0), autoSave=True, partialSave=True, transmit=True) noise = UntypedReference(autoSave=True, partialSave=True, transmit=True) offset = FloatVector(default=(0, 0, 0), autoSave=True, partialSave=True, transmit=True) def __init__(self, seed=None, persistence=0.5, octaves=4, map=None, multiplier=(5.0, 100.0, 4.0), noise=None, offset=(0, 0, 0)): Persistable.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.multiplier = multiplier self.offset = offset self.noise = noise or PerlinNoise(seed=seed or int(time()), persistence=persistence, octaves=octaves) self.noise.grantPermission("read", "all") self.noise.grantPermission("reference", "all") if map is not None: self.noise.map = map def generate(self, pos): """ Generate a height using our noise generator """ p = ((pos[0] + self.offset[0]) * self.multiplier[0], (pos[1] + self.offset[2]) * self.multiplier[2]) n = (self.noise.perlinNoise(p) + self.offset[1]) * self.multiplier[1] return n def setOffset(self, offset): """ Sets the offset """ self.offset = offset def setMultiplier(self, multiplier): """ Sets the multiplier """ self.multiplier = multiplier def getClassGenerators(self): return [ClassGenerator().setFrom(self), ClassGenerator().setFrom( self.noise)] class IMeshGen: """ Interface definition for mesh generators """ def genMesh(self, lodfactor=1.0, justTris=True): """ Generate the mesh. If justTris is False, then normals and uv coords, and color map will be generated as well """ class MeshLodLevel: """ Defines a mesh at a specific lod """ def __init__(self, verts=None, colors=None, norms=None, uvs=None, tris=None, trinorms=None, justTris=False): self.verts = verts self.tris = tris self.colors = colors self.norms = norms self.uvs = uvs self.trinorms = trinorms self.justTris = justTris class HeightField(IMeshGen, Cacheable, Securable, Persistable): """ A heightfield mesh """ stoppedObserving = Cacheable.stoppedObserving heightGenerator = UntypedReference(transmit=True) lodlevels = None size = FloatVector(default=(1.0, 1.0, 1.0), transmit=True) resolution = IntVector(default=(10, 10, 10), transmit=True) heights = FloatVector(transmit=True) def __init__(self, heightGenerator=None, size=None, resolution=None, heights=None): Persistable.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.heightGenerator = heightGenerator self.size = size self.resolution = resolution self.heights = heights or array('f') self.lodlevels = dict() def onLoad(self): """ Called when loaded from the datastore. """ self.lodlevels = dict() self.heights = array('f', self.heights) def setHeights(self, resolution, heights): """ Sets the heights of this HeightField """ if isinstance(heights, str): heightArray = array('f') heightArray.fromstring(heights) heights = heightArray self.resolution = resolution self.heights = heights self.lodlevels = dict() self.updateAllClients("setHeights", resolution, heights.tostring()) self.queueSave(["heights", "resolution"]) def observe_setHeights(self, resolution, heights): """ This method decompresses the heights from an array encoded into a string """ heightArray = array('f') heightArray.fromstring(heights) return self.setHeights(resolution, heightArray) @withClientUpdate def setHeightGenerator(self, heightGenerator): """ Set our height Generator """ self.heightGenerator = heightGenerator self.queueSave(["heightGenerator"]) @withClientUpdate def setResolution(self, resolution): """ Set our resolution """ self.resolution = resolution self.queueSave(["resolution"]) @withClientUpdate def setSize(self, size): """ Set our size """ self.size = size self.queueSave(["size"]) observe_setHeightGenerator = setHeightGenerator observe_setResolution = setResolution observe_setSize = setSize def calcIndex(self, point): """ Calc the index in our height array of point (x,y) """ if (point[0] < 0 or point[1] < 0 or point[0] > self.resolution[0] or point[1] > self.resolution[1]): raise IndexError("Point %r is not on height field" % (point,)) return (int(point[0]) + int(point[1]) * self.resolution[0]) def getHeight(self, point): """ Get the height at this point """ return self.heights[int(self.calcIndex(point))] @withClientUpdate def setHeight(self, point, value): """ Set the height at this point """ self.lodlevels = dict() self.heights[int(self.calcIndex(point))] = value self.queueSave(selectAttributes=["heights"]) observe_setHeight = setHeight def getSmoothedHeight(self, point, useGenerator=True): """ Get the height at this point, but smooth it out with bilinear interpolation """ if self.heightGenerator is not None and useGenerator: return self.heightGenerator.generate((point[0] / float( self.resolution[0]), point[1] / float(self.resolution[1]))) tw = self.resolution[0] - 1 th = self.resolution[1] - 1 ww = point[0] hh = point[1] iw = int(ww) ih = int(hh) if iw == tw and ih == th: return self.getHeight(point) pw = ww - iw ph = hh - ih interp = lambda a, b, p: (a * (1.0 - p)) + (b * p) if ih >= th: if iw >= tw: return self.heights[tw + th * self.resolution[0]] return interp(self.heights[iw + th * self.resolution[0]], self.heights[iw + 1 + th * self.resolution[0]], pw) if iw >= tw: return interp(self.heights[tw + ih * self.resolution[0]], self.heights[tw + (ih + 1) * self.resolution[0]], ph) h00 = self.heights[iw + ih * self.resolution[0]] h01 = self.heights[iw + (ih + 1) * self.resolution[0]] h10 = self.heights[iw + 1 + ih * self.resolution[0]] h11 = self.heights[iw + 1 + (ih + 1) * self.resolution[0]] ww1 = interp(h00, h01, ph) ww2 = interp(h10, h11, ph) return interp(ww1, ww2, pw) def genMesh(self, lodfactor=1.0, justTris=True, forceRecalc=False): """ This will generate the mesh. If justTris is False, then it will only generate the triangles. """ if (not forceRecalc and self.lodlevels.has_key(lodfactor) and self.lodlevels[lodfactor].justTris == justTris): return defer.succeed(self.lodlevels[lodfactor]) if lodfactor != 1.0: return self.genMeshAtLOD(lodfactor, justTris, forceRecalc) if len(self.heights) == 0 and self.heightGenerator is not None: self.generateHeights() verts = [] if not justTris: colors = [(1, 1, 1)] * (self.resolution[0] * self.resolution[1]) norms = [(0, 0, 0)] * (self.resolution[0] * self.resolution[1]) else: colors = [] norms = [] uvs = [] tris = [] trinorms = [] self.lodlevels[lodfactor] = MeshLodLevel(verts, colors, norms, uvs, tris, trinorms, justTris) vertc = 0 for z in frange(0, self.size[1], self.resolution[1]): for x in frange(0, self.size[0], self.resolution[0]): y = self.heights[vertc] # print vertc verts.append(Vector(x, y, z)) # print tuple(verts[-1]) if not justTris: uvs.append((x / float(self.size[0]), z / float( self.size[1]))) vertc += 1 for tri in range(self.resolution[0] * self.resolution[1] - ( 1 + self.resolution[1])): if divmod(tri, self.resolution[0])[1] == self.resolution[0] - 1: continue tris.append((tri, tri + self.resolution[0], tri + self.resolution[0] + 1)) tris.append((tri + 1, tri, tri + 1 + self.resolution[0])) if not justTris: v3 = verts[tri + self.resolution[0] + 1] v2 = verts[tri + self.resolution[0]] v1 = verts[tri] s1 = v1 - v2 s2 = v2 - v3 v3 = verts[tri + self.resolution[0] + 1] v2 = verts[tri] v1 = verts[tri + 1] trinorms.append((s1 ** s2).normalize()) norms[tri + self.resolution[0] + 1] = Vector(norms[ tri + self.resolution[0] + 1]) + trinorms[-1] norms[tri + self.resolution[0]] = Vector(norms[ tri + self.resolution[0]]) + trinorms[-1] norms[tri] = Vector(norms[tri]) + trinorms[-1] s3 = v2 - v1 s4 = v3 - v2 trinorms.append((s3 ** s4).normalize()) # print tris return defer.succeed(self.lodlevels[lodfactor]) def genMeshAtLOD(self, lodfactor=1.0, justTris=True, forceRecalc=False): """ This will generate the mesh. If justTris is False, then it will only generate the triangles. """ if (not forceRecalc and self.lodlevels.has_key(lodfactor) and self.lodlevels[lodfactor].justTris == justTris): return defer.succeed(self.lodlevels[lodfactor]) if len(self.heights) == 0 and self.heightGenerator is not None: self.generateHeights() verts = [] rez = (int(self.resolution[0] * lodfactor), int( self.resolution[1] * lodfactor)) if not justTris: colors = [(1, 1, 1)] * (rez[0] * rez[1]) norms = [(0, 0, 0)] * (rez[0] * rez[1]) else: colors = [] norms = [] uvs = [] tris = [] trinorms = [] self.lodlevels[lodfactor] = MeshLodLevel(verts, colors, norms, uvs, tris, trinorms, justTris) for z in frange(0, 1, rez[1]): for x in frange(0, 1, int(self.resolution[0] * lodfactor)): y = self.getSmoothedHeight((x * self.resolution[0], z * self.resolution[1])) verts.append(Vector(x * self.size[0], y, z * self.size[1])) if not justTris: # print "uvs" uvs.append((x, z)) for tri in range(rez[0] * rez[1] - (1 + rez[1])): if divmod(tri, rez[0])[1] == rez[0] - 1: continue tris.append((tri, tri + rez[0], tri + rez[0] + 1)) tris.append((tri + 1, tri, tri + 1 + rez[0])) if not justTris: v3 = verts[tri + rez[0] + 1] v2 = verts[tri + rez[0]] v1 = verts[tri] s1 = v1 - v2 s2 = v2 - v3 v3 = verts[tri + rez[0] + 1] v2 = verts[tri] v1 = verts[tri + 1] trinorms.append((s1 ** s2).normalize()) norms[tri + rez[0] + 1] = ( Vector(norms[tri + rez[0] + 1]) + trinorms[-1]) norms[tri + rez[0]] = Vector(norms[tri + rez[0]]) + trinorms[-1] norms[tri] = Vector(norms[tri]) + trinorms[-1] s3 = v2 - v1 s4 = v3 - v2 trinorms.append((s3 ** s4).normalize()) return defer.succeed(self.lodlevels[lodfactor]) def generateHeights(self, heightGenerator=None, resolution=None): """ Generate heights for this height field using a generator """ if heightGenerator is not None: self.setHeightGenerator(heightGenerator) if resolution is not None: self.setResolution(resolution) self.heights = array('f') for z in frange(0, 1, self.resolution[1]): for x in frange(0, 1, self.resolution[0]): self.heights.append(self.heightGenerator.generate((x, z))) self.updateAllClients("setHeights", self.heights.tostring()) return defer.succeed(True) def translateWorldToHeightField(self, world, body): """ Translate a point in world coordinates to a height field position on the terrain. """ location = body.getRotation() * (Vector(world) - body.getPosition()) return (location[0] / self.size[0] * self.resolution[0], location[2] / self.size[1] * self.resolution[1]) def translateWorldToTexture(self, world, body): """ Translate a point in world coordinates to a uv position on the terrain texture. """ location = body.getRotation() * (Vector(world) - body.getPosition()) return (location[0] / self.size[0] * 128.0, location[2] / self.size[1] * 128.0) def getClassGenerators(self): """ Get all the class generators needed for this meshGen """ cgs = [ClassGenerator().setFrom(self)] if self.heightGenerator is not None: cgs.extend(self.heightGenerator.getClassGenerators()) return cgs def getStateToCacheAndObserveFor(self, p, o): """ When loading from a remote server, we decompress the heights from a string which has an array encoded into it. """ state = Cacheable.getStateToCacheAndObserveFor(self, p, o) if state.has_key("datastore"): del state["datastore"] # if self.heightGenerator is not None: # state["heights"] = None # else: state["_auto_heights"] = self.heights.tostring() return state def setCopyableState(self, state): """ When copying to a remote server, we compress the heights into an array encoded to a string """ convertAutoSave(state) hts = state["heights"] state["heights"] = array('f') if hts is not None: state["heights"].fromstring(hts) Cacheable.setCopyableState(self, state) class MeshGenStaticBody(BodyWithColliders, Cacheable, Securable): """ A static body that uses a mesh generator """ _schemaVersion = 1 getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor stoppedObserving = Cacheable.stoppedObserving setCopyableState = Cacheable.setCopyableState space = None world = None special = Boolean(default=False, autoSave=True, partialSave=True, transmit=True) areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) area = None transformGeom = None collider = None transformedcollider = None position = FloatVector(default=(0, 0, 0), autoSave=True, partialSave=True, transmit=True) rotation = FloatVector(default=(1, 0, 0, 0), autoSave=True, partialSave=True, transmit=True) meshGen = UntypedReference(autoSave=True, partialSave=True, transmit=True) physlod = Float(default=1.0, autoSave=True, partialSave=True, transmit=True) vobs = List(UntypedReference(), transmit=True) objectid = IDTuple(autoSave=True, partialSave=True, transmit=True) neighbors = MapAttribute(Text(), IDTuple(), transmit=True) def __init__(self, **keys): BodyWithColliders.__init__(self) Cacheable.__init__(self) Securable.__init__(self) for k, v in keys.iteritems(): if not hasattr(self.__class__, k): raise ValueError("Inalid keyword argument %s to %s" % (k, self.__class__.__name__)) setattr(self, k, v) self.vobs = [] def create(self, world=None, meshGen=None, space=None): """ Create the the colliders if space is defined """ if meshGen is not None: self.meshGen = meshGen self.world = world if self.meshGen is not None and self.collider is None: self.collider = world.getPhysicsClass(IMeshGenCollider)( meshGen=self.meshGen, lodfactor=self.physlod) self.collider.setOID(self.objectid) if space is not None: self.enterSpace(space) def destroy(self): """ Destroy the colliders """ if self.collider is not None: self.collider.destroy() for vob in self.vobs: vob.destroy() self.area = None self.space = None @withClientUpdate def rebuild(self): """ Called when the meshGen changes and causes physical and visual parts to be rebuilt """ area = self.area self.destroy() self.enterArea(area) observe_rebuild = rebuild @withClientUpdate def setMeshGen(self, meshGen): """ Set the mesh generator """ self.meshGen = meshGen def getMeshGen(self): """ Returns the meshGen """ return self.meshGen def setScale(self, scale): """ TODO: Implement """ @withClientUpdate def setPhysicalLod(self, lodfactor): """ Set the physical lodfactor """ self.physlod = lodfactor observe_setMeshGen = setMeshGen observe_setPhysicalLod = setPhysicalLod def getPreferredSpaceType(self): """ Return our preferred space type 1 = Disabled 2 = Special """ if self.special: return 2 return 1 @withClientUpdate def setPosition(self, pos): """ Set the position of this body """ self.position = pos self.updateVobs() self.enterSpace(self.space) def getPosition(self): """ Get the position of this body """ return self.position @withClientUpdate def setRotation(self, rot): """ Set the quaternion rotation of this body """ self.rotation = rot self.updateVobs() self.enterSpace(self.space) def getRotation(self): """ Get the quaternion rotation of this body """ return self.rotation observe_setPosition = setPosition observe_setRotation = setRotation def enable(self): """ Can't be enabled """ def disable(self): """ Can't be disabled """ def isEnabled(self): """ We can't be enabled """ return False def enterSpace(self, space): """ Create colliders in this space """ if self.collider is None: if self.meshGen is not None: self.collider = self.world.getPhysicsClass(IMeshGenCollider)( meshGen=self.meshGen, lodfactor=self.physlod, oid=self.objectid) else: return if self.transformedcollider is not None: self.transformedcollider.destroy() self.transformedcollider = self.collider.copy() self.transformedcollider.setPosition(Vector( self.collider.position) + self.position) self.transformedcollider.setRotation(Quaternion( self.collider.rotation) * self.rotation) self.transformedcollider.setOID(self.objectid) self.transformedcollider.build(space=space) self.space = space def enterArea(self, area): """ Determine the space most appropriate and enter it. Area should be an area object """ if self.special: space = area.getSpecialSpace(self.getPosition()) else: space = area.getDisabledSpace(self.getPosition()) self.world = area.world self.enterSpace(space) self.areaid = area.getID() self.area = area dlist = [] for vob in self.vobs: dlist.append(maybeDeferred(vob.create, area, self.getPosition(), self.getRotation())) return gatherResults(dlist) def leaveArea(self, _area): """ Just destroy ourselves """ return self.destroy() @withClientUpdate def addVisualObject(self, vob): """ Add a visual object """ self.vobs.append(vob) vob.create(self.area, self.getPosition(), self.getRotation()) self.queueSave(selectAttributes=["vobs"]) def addGrassVisualObject(self): """ Add some grass to this mesh """ pos = self.getPosition() size = self.meshGen.size bounds = (pos[0], pos[2], pos[0] + size[0], pos[2] + size[1]) grass = Grass(bounds, self.meshGen) grass.grantPermission("read", "all") grass.grantPermission("reference", "all") self.addVisualObject(grass) return grass @withClientUpdate def removeVisualObject(self, vob): """ Remove a visual object """ self.vobs.remove(vob) self.queueSave(selectAttributes=["vobs"]) @withClientUpdate def setNeighbors(self, neighbors): """ Neighbors are defined as any nearby generated object that should share some vertex data with this one. If vertex data on one is changed, it should change on the other. It's a dict of item ids. For example, a terrain object would define dict(north=(0,1), south=(0,2), east=(0,3), west=(0,4) to specify which other terrains border this one. """ self.neighbors = neighbors self.queueSave(selectAttributes=["neighbors"]) observe_removeVisualObject = removeVisualObject observe_addVisualObject = addVisualObject observe_setNeighbors = setNeighbors def getVisualObjects(self): """ Get all the visual objects """ return self.vobs def updateVobs(self): """ Update all the visual objects """ pos = self.getPosition() rot = self.getRotation() for vob in self.vobs: if isinstance(vob, IVisualObjectView): vob.setPosition(pos) vob.setRotation(rot) def getServerClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) cgs += self.meshGen.getClassGenerators() for vob in self.vobs: cgs += vob.getServerClassGenerators() return cgs def getClientClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) cgs += self.meshGen.getClassGenerators() for vob in self.vobs: cgs += vob.getClientClassGenerators() return cgs class MeshGenVisualObject(IVisualObject, Cacheable, Securable, Persistable): """ A Visual Object that uses a mesh generator """ stoppedObserving = Cacheable.stoppedObserving getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor setCopyableState = Cacheable.setCopyableState areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) meshGen = UntypedReference(autoSave=True, partialSave=True, transmit=True) materialAssetId = IDTuple(autoSave=True, partialSave=True, transmit=True) textureLayerAssetIds = List(IDTuple(), transmit=True) textureLayerSettings = List(Pickled(), transmit=True) textureLayerMaps = Pickled(autoSave=True, partialSave=True, transmit=True) def __init__(self, areaid=None, meshGen=None, materialAssetId=None, textureLayerAssetIds=None, textureLayerSettings=None, textureLayerMaps=None): Persistable.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.areaid = areaid self.meshGen = meshGen self.materialAssetId = materialAssetId self.textureLayerAssetIds = textureLayerAssetIds self.textureLayerSettings = textureLayerSettings self.textureLayerMaps = textureLayerMaps def create(self, _area, _pos_, _rot): """ This does nothing on the server side """ def enterArea(self, area): """ Enter an area """ self.areaid = area.getID() def getMeshGen(self): """ Returns the mesh generator """ return self.meshGen @withClientUpdate def setMeshGen(self, meshGen): """ Set the mesh generator """ self.meshGen = meshGen def getMaterialAsset(self): """ Returns the material asset id """ return self.materialAssetId @withClientUpdate def setMaterialAsset(self, aid): """ Set the material asset for the meshgen """ self.materialAssetId = aid @withClientUpdate def setTextureLayerMaps(self, maps): """ Set all the texture layer maps """ self.textureLayerMaps = maps @withClientUpdate def setTextureLayerMap(self, layer, map): """ Set the texture layer map """ self.textureLayerMaps[layer] = map self.queueSave(selectAttributes=["textureLayerMaps"]) @withClientUpdate def setTextureLayerAssetId(self, layer, assetId): """ Set the texture layer asset id for a layer """ self.textureLayerAssetIds[layer] = assetId self.queueSave(selectAttributes=["textureLayerAssetIds"]) @withClientUpdate def setTextureLayerSettings(self, layer, settings): """ Sets the settings for a given layer """ self.textureLayerSettings[layer] = settings self.queueSave(selectAttributes=["textureLayerSettings"]) @autoStore @withClientUpdate def addTextureLayer(self, assetId, settings, map_): """ Add a new texture layer """ if self.textureLayerAssetIds is None: self.textureLayerAssetIds = [] if self.textureLayerMaps is None: self.textureLayerMaps = [] if self.textureLayerSettings is None: self.textureLayerSettings = [] self.textureLayerAssetIds.append(assetId) self.textureLayerSettings.append(settings) self.textureLayerMaps.append(map_) @autoStore @withClientUpdate def removeTextureLayer(self, layer): """ Remove the given texture layer """ self.textureLayerAssetIds.pop(layer) self.textureLayerMaps.pop(layer) self.textureLayerSettings.pop(layer) observe_setMeshGen = setMeshGen observe_setMaterialAsset = setMaterialAsset observe_setTextureLayerMaps = setTextureLayerMaps observe_setTextureLayerAssetId = setTextureLayerAssetId observe_setTextureLayerSettings = setTextureLayerSettings observe_addTextureLayer = addTextureLayer observe_removeTextureLayer = removeTextureLayer def getServerClassGenerators(self): """ Returns class generators for a server """ return [ClassGenerator(classname="MeshGenVisualObject", modulename="mv3d.phys.mesh", sourceclass="mv3d.phys.mesh.MeshGenVisualObject")] def getClientClassGenerators(self): """ Returns class generators for a client """ return [ClassGenerator(classname="MeshGenVisualObjectView", modulename="mv3d.client.view.visual", sourceclass="mv3d.phys.mesh.MeshGenVisualObject")]