Jump to content
The Dark Mod Forums

SteveL

Member
  • Posts

    3666
  • Joined

  • Last visited

  • Days Won

    62

Posts posted by SteveL

  1. One of the intermediate stages of dmapping looks like a subtractive geometry map -- the stage after it's finished working out which (parts of) brush faces can be reached by entities and then it's stripped away all the other brush faces.

     

    I've never used any other editor so I have no idea whether subtractive mapping would be compatible with our workflow. But all our game assets are set up for DR so I can't imagine it being possible to translate them into another editor. You could make some worldspawn in another editor maybe, but it wouldn't be compatible with dmap which expects the world to be made out convex brushes (actually what gets stored is the map file is just planes, and dmap reconstructs your brushes from where those planes intersect).

     

    What's the advantage of the subtractive method? One advantage of our additive method is it forces the mapper to fix their leaks rather than leaving it to the game engine to make guesses (often inefficient).

  2. It doesn't handle lights as such, so lights in the water would be treated the same as any light. Lights light surfaces, and surfaces behind the water have their resulting colour filtered by the shader according to how much water there is in the way. There'd be no glow in the water itself, just light from whatever surfaces get lit.

  3. Grand, the only issue with sea_water_001, its appears to glow so as a result the small boat looks like its floating in place rather than on the water.

     

    The best fix I've found so far is to use my original material above but reduce the rgb of the two water layers to 0.1 and 0.05.

     

    I've made a new test map with lots of water channels and I've started to play around with it. I've also made a shader program that can fade out the water to a set colour independent of the surface overlays. It's not added as much flexibility as I thought, for the sea water anyway... the original looked better. I'm still on sea water but most of our waters work quite differently. Sea water uses a filtering blend, so it always darkens the background. We might find other waters work better with the colour-fade shader. Still messing with the options, but in the meantime see what you think of the reduced-rgb version of the material I posted above.

    • Like 2
  4. What value in the material def do I edit toe change how far the player can see through the water..? I guess I change the current value of 48 in the following line -

     

    - vertexParm 2 ( 1.0 / 48.0 ) // Set this to ( 1.0 / opaque_depth )

     

    Also I'd like to try this new technique on a few other water textures -

     

    - textures/water_source/water_green

    - textures/water_source/water_blue

    - textures/water_source/water_colored

    - textures/water_source/water_dark

     

    So with the depth set to 384, I took the following wip shots -

     

    Cool, they're looking good already even before we experiment with method #2 and other shaders. Yes as you discovered that's how to change the murkiness. I'll change it in the next version so you just put the depth in instead of ( 1.0 / depth ) which is unnecessarily confusing.

     

    I plan to put a more waters in a test map and try tweaking it for each. The ones you called out look like a good selection to start with.

    • Like 1
  5. I want to try a different way of implementing it, but I'll post the WIP for Biker and anyone wanting to experiment with it.

     

    Material file

    This is for your materials/ folder

    semiopaque.mtr.txt

     

    Shader program

    This needs to go in a glprogs/ folder alongside your other top-level folders (maps/, materials/ etc).

    heatHazeWithAlphaDepth.vfp.txt

     

    __

     

    How it works

    The above shader draws a value 0.0 to 1.0 in the alpha channel of your screen. It says for each pixel, what % of the water shader to use and what % of the background colour to show. The two always add up to 100%. So if you set the opaque distance in the material file to 48 units (like it is in my example), and the distance between the water surface and the background for a given pixel is 16 units, then the alpha for that pixel will get set to 16 / 48 = 0.333, and you'll get 33% of the seawater shader and 66% of the background in the colour mix for that pixel. That's what "blend gl_dst_alpha, gl_one_minus_dst_alpha" means in the shader: multiply the colour from the current texture by the alpha that's already in the screen buffer (destination alpha) and the background colour by 1 minus the destination alpha.

     

    Next thing I want to try

    That method might be too restrictive.. It's reduced the contrast in the seawater texture quite a lot. It's made the blue surface water brighter and the highlights duller. I want to try something else, although I haven't really tried to fix it yet. We could make the second draw of the water texture independent of the alpha, for example. But the thing I want to try next is to get the shader to adjust the background colour directly instead of just setting up how it merges with the water surface texture. So we'd set an extra parameter in the material to indicate the opaque water colour. The shader would fade the background to that colour and not bother drawing anything into the alpha channel... you'd then just apply whatever water surface detail you want as a separate stage. It might be easier to adjust the water brightness that way. And it would be simpler too.

     

    EDIT: Hmm we would lose the soft edge on the seawater that way. I guess we can leave both options open. Paint the alpha values, which the rest of the material can choose to use or ignore, and paint the water colour independently, again optional. I'll put together a few examples in another video so we can see what they look like.

    • Like 1
  6. Man that has got to be the fastest fix, for the smallest effort for the best visuals, to date...?

     

    Heh I spent a good 90 minutes on that in total!

     

    Because this is now a fix to the distortion shader that all waters use, we could in theory drop it into all existing water shaders like we did the fix for foreground distortion. You can set the depth at which the water looks completely opaque, either in the material file or with a shaderparm. For very clear water, you'd set it to something big like 1000000.

     

    One potential fly in the ointment: I think people have achieved similar effects in the past with dark underwater fog lights, although that's buggy from a distance whereas this shouldn't be. But we can't just go changing all the default waters without checking what those look like first... it might be a bad combination in which case we'd have to content ourselves with some new murky shaders instead of upgrading old ones.

     

    I have to go cook but I'll tidy up the new vertex program and material file and post them here later. There's no engine change for this, it's just a couple of extra files that mappers can play around with and package with their map if they wish.

  7. I take it this is incorrect, then:

     

    You can have only one shader program of each type in any one material stage / draw call, but materials can have multiple stages.

     

    The original sea water 01 material had 3 draws: one to distort the background, and two layers of sea water.

     

    The material I showed in the vid bumped that up to 4 draw calls for the water surface: The fixed version is back to 3, because the alpha layer is now drawn at the same time as the distortion. I merged the distortion shader program with the depth-testing one.

  8. Cool. I haven't a clue how to put all this information into a readable article tbh, and it might be better to leave the inherited spawnargs out. They mean 100s of 1000s of duplicated spawnargs. Maybe you could structure your article around the inheritance tree, so you need only quote each distinct spawnarg once.

     

    If you find yourself faced with a massive manual task at any point that could be automated, feel free to ask for a script change :)

  9. I finally tried this code myself, and I've added a couple of new settings at the top of the script:

    • DEBUGMODE = False
    • INCLUDE_INHERITED_SPAWNARGS = True
    • INCLUDE_DR_HELP_TEXT = False
    • Setting DEBUGMODE to False suppresses screen output. You just get a single-line confirmation at the end, plus the .csv file that you can load in your spreadsheet. That makes the script run much faster.
    • Setting INCLUDE_INHERITED_SPAWNARGS to True will show all inherited spawnargs, so you can see the full list that each AI def uses. There's a fourth column in the output that tells you whether the spawnarg is inherited or not so you can filter them in your s/sheet.
    • Setting INCLUDE_DR_HELP_TEXT to False will hide any "editor_*" spawnargs.
    Warning: including inherited s/args and help text means you'll get 500k records! Without the help text but including inherited spawnargs, it's about 250k.

     

     

     

    import re, os, fnmatch, zipfile
    from collections import Counter
     
    GAMEPATH = r"C:\darkmod"
    FMPATH = r""
    DEBUGMODE = False
    INCLUDE_INHERITED_SPAWNARGS = True
    INCLUDE_DR_HELP_TEXT = False
     
    # regexes
    NAME_PTN = re.compile(r'entityDef\s+(\S+)\s*\{.*?\}', re.IGNORECASE | re.DOTALL)
    ENT_PTN = re.compile(r'(entityDef\s+(\S+)\s*\{.*?\})', re.IGNORECASE | re.DOTALL)
    ARGS_PTN = re.compile(r'\{([^}]*)\}', re.IGNORECASE)
    SPAWNARG_PTN = re.compile(r'("[^"\n]*"|[^\s\n"]+)')
     
     
    def searchPk4(pk4, exts):
        """Look in a pk4 archive and yield files with any of the extensions 
        in exts. Returns ( filename, filecontents ). filename includes any
        directory path internal to the archive.
        
        pk4: full path, e.g. r'E:\darkmod\tdm_defs01.pk4'
        exts: list or tuple of bare extensions, e.g. ['def', 'mtr']
        """
        try:
            f = zipfile.ZipFile(pk4, 'r')
        except zipfile.BadZipfile:
            print "*** Error reading ", pk4
            return
        for member in f.infolist():
            ext = (os.path.splitext(member.filename)[1]).lower()
            if ext in exts:
                yield member.filename, f.read(member)
     
    def findTDMFiles(paths, exts):
        """Yield tuples: (filepath, filecontents) of all files 
        found in paths with any of the extensions in exts. Looks inside
        pk4 archives, but not nested archives.
        
        paths, exts are sequences containing one or more paths / extensions.
        """
        # Make sure params are both lists else we'll get weird results from a string
        assert(paths.append and exts.append)
        for path in set(paths):
            for root, dirnames, filenames in os.walk(path):
                for ext in set( ['.pk4'] + exts):
                    for filename in fnmatch.filter(filenames, '*'+ext):
                        fpath = os.path.join(root, filename)
                        if ext == '.pk4':
                            for internalname, content in searchPk4(fpath, exts):
                                yield ( fpath + ' -> ' + internalname,   content )
                        else:
                             yield fpath, file(fpath).read()
     
    def debugprint(text):
        if DEBUGMODE:
            print text
     
    def stripComments(text):
        """
        Strip text of /* block comments */ and // line comments, 
        except for those in a "string".
        """
        result = ''
        context = 'normal'
        pos = 0
        while pos < len(text):
            token = text[pos]
            next  = text[pos+1] if pos < len(text)-1 else ''
            skip = 0
            if context == 'string':
                result += token
                if token == '"':
                    context = 'normal'
            elif context == 'line comment':
                if token == '\n':
                    context = 'normal'
                    result += token
            elif context == 'block comment':
                if token + next == '*/':
                    context = 'normal'
                    skip = 1
            else:
                assert(context == 'normal')
                if token + next == '//':
                    context = 'line comment'
                    skip = 1
                elif token + next == '/*':
                    context = 'block comment'
                    skip = 1
                else:
                    result += token
                    if token == '"':
                        context = 'string'
            pos += (1 + skip)
        return result
     
    class entityDef:
        def __init__(self, name, filename, parentname='', spawnargs={}, inherited={}):
            self.name = name
            self.file = filename
            self.parentname = parentname
            self.parents = []      # Set by calling setParents() once all defs constructed
            self.children = set()  # Set by calling setParents() on all defs once constructed
            self.spawnargs=spawnargs
            self.inherited=inherited
     
        def _findParents(self, defs):
            """Return a list of parents, starting with the immediate parent."""
            parent = defs.get( self.spawnargs.get('inherit') )
            if parent:
                return [parent] + parent._findParents(defs)
            else:
                return []
     
        def setParents(self, defs):
            """Discover our ancestors and collect their spawnargs. 
            Make an inheritance list then work down it
            so spawnargs get overridden right, finally adding the our own."""
            self.parents = self._findParents(defs)
            for p in reversed(self.parents):
                self.inherited.update(p.spawnargs)
                p.children.add(self)
     
    def getFileDefs(fname, text):
        """Return a dict: { name : entityDef }"""
        text = stripComments(text)
        entdefs = ENT_PTN.findall(text) # [ ( deftext, name ), ... ]
        defs = {}
        for deftext, name in entdefs:
            spawnargs = {}
            sp_text = ARGS_PTN.search(deftext).group(1)
            #debugprint(sp_text)
            strings = SPAWNARG_PTN.findall(sp_text)
            #debugprint('%s Strings found: %d' % (name, len(strings)))
            for idx in range(0, len(strings), 2):
                spawnargs[strings[idx].strip('"').lower()] = strings[idx+1].lower().strip('"')
            parentname = spawnargs.get('inherit')
            defs[name] = entityDef(name, fname, parentname, spawnargs)
        return defs
     
    def getDefs(path, exclude_fms=False):
        """Compile a dict of entity defs from path: { name : entityDef }"""
        debugprint('Checking path ' + path)
        defs = {}   # { name : entityDef }
        for fname, text in findTDMFiles( [path] , exts=['.def']):
            if exclude_fms and '\\fms\\' in fname:
                pass
            else:
                debugprint('Checking file ' + fname)
                defs.update(getFileDefs(fname, text))
                #debugprint('Def count %d' % len(defs))
        # Now we have all defs, populate ancestor details and inherited spawnargs
        for name, e in defs.items():
            e.setParents(defs)
        return defs
     
    if __name__ == '__main__':
        defs = getDefs(GAMEPATH, exclude_fms=True)
        filecount = len( set( d.file for d in defs.values() ) )
        print "Found %d files with %d defs" % (filecount, len(defs))
        # Print out a csv table of AI (anything inheriting from atdm:ai_base
        results = ['"Name","Spawnarg","Default value","Inherited"']
        ai_base = defs["atdm:ai_base"]
        for ent in ai_base.children:
            sp_args = [s for s in ent.spawnargs if INCLUDE_DR_HELP_TEXT or not s.startswith('editor_')]
            for s in sp_args:
                results.append('"'+ent.name+'","'+s+'","'+ent.spawnargs[s]+'","N"')
            active_inherited_spawnargs = [s for s in ent.inherited
                                          if INCLUDE_INHERITED_SPAWNARGS
                                          and s not in ent.spawnargs
                                          and (INCLUDE_DR_HELP_TEXT or not s.startswith('editor_'))]
            for s in active_inherited_spawnargs:
                results.append('"'+ent.name+'","'+s+'","'+ent.inherited[s]+'","Y"')
        for row in results:
            debugprint(row)
        outfile = file('ai_spawnargs.csv', 'w')
        outfile.writelines(r + '\n' for r in results)
        outfile.close()
    

     

  10. I don't know why it can't reach the mapparse function. Maybe I screwed up the syntax somehow, although I usually make it clear if I post code without testing it.

    You could avoid the problem by merging the two scripts.

    Start with ai_spawnarg,py
    Add zipfile to the list of imports
    Paste the 2 functions from mapparse_general above the functions in the ai script.

    I mean like this (not tested but hopefully ok):

     

    import re, os, fnmatch, zipfile
    from collections import Counter
     
    GAMEPATH = r"C:\darkmod"
    FMPATH = r""
    DEBUGMODE = True
     
    # regexes
    NAME_PTN = re.compile(r'entityDef\s+(\S+)\s*\{.*?\}', re.IGNORECASE | re.DOTALL)
    ENT_PTN = re.compile(r'(entityDef\s+(\S+)\s*\{.*?\})', re.IGNORECASE | re.DOTALL)
    ARGS_PTN = re.compile(r'\{([^}]*)\}', re.IGNORECASE)
    SPAWNARG_PTN = re.compile(r'("[^"\n]*"|[^\s\n"]+)')
     
     
    def searchPk4(pk4, exts):
        """Look in a pk4 archive and yield files with any of the extensions 
        in exts. Returns ( filename, filecontents ). filename includes any
        directory path internal to the archive.
        
        pk4: full path, e.g. r'E:\darkmod\tdm_defs01.pk4'
        exts: list or tuple of bare extensions, e.g. ['def', 'mtr']
        """
        try:
            f = zipfile.ZipFile(pk4, 'r')
        except zipfile.BadZipfile:
            print "*** Error reading ", pk4
            return
        for member in f.infolist():
            ext = (os.path.splitext(member.filename)[1]).lower()
            if ext in exts:
                yield member.filename, f.read(member)
     
    def findTDMFiles(paths, exts):
        """Yield tuples: (filepath, filecontents) of all files 
        found in paths with any of the extensions in exts. Looks inside
        pk4 archives, but not nested archives.
        
        paths, exts are sequences containing one or more paths / extensions.
        """
        # Make sure params are both lists else we'll get weird results from a string
        assert(paths.append and exts.append)
        for path in set(paths):
            for root, dirnames, filenames in os.walk(path):
                for ext in set( ['.pk4'] + exts):
                    for filename in fnmatch.filter(filenames, '*'+ext):
                        fpath = os.path.join(root, filename)
                        if ext == '.pk4':
                            for internalname, content in searchPk4(fpath, exts):
                                yield ( fpath + ' -> ' + internalname,   content )
                        else:
                             yield fpath, file(fpath).read()
     
    def debugprint(text):
        if DEBUGMODE:
            print text
     
    def stripComments(text):
        """
        Strip text of /* block comments */ and // line comments, 
        except for those in a "string".
        """
        result = ''
        context = 'normal'
        pos = 0
        while pos < len(text):
            token = text[pos]
            next  = text[pos+1] if pos < len(text)-1 else ''
            skip = 0
            if context == 'string':
                result += token
                if token == '"':
                    context = 'normal'
            elif context == 'line comment':
                if token == '\n':
                    context = 'normal'
                    result += token
            elif context == 'block comment':
                if token + next == '*/':
                    context = 'normal'
                    skip = 1
            else:
                assert(context == 'normal')
                if token + next == '//':
                    context = 'line comment'
                    skip = 1
                elif token + next == '/*':
                    context = 'block comment'
                    skip = 1
                else:
                    result += token
                    if token == '"':
                        context = 'string'
            pos += (1 + skip)
        return result
     
    class entityDef:
        def __init__(self, name, filename, parentname='', spawnargs={}, inherited={}):
            self.name = name
            self.file = filename
            self.parentname = parentname
            self.parents = []      # Set by calling setParents() once all defs constructed
            self.children = set()  # Set by calling setParents() on all defs once constructed
            self.spawnargs=spawnargs
            self.inherited=inherited
     
        def _findParents(self, defs):
            """Return a list of parents, starting with the immediate parent."""
            parent = defs.get( self.spawnargs.get('inherit') )
            if parent:
                return [parent] + parent._findParents(defs)
            else:
                return []
     
        def setParents(self, defs):
            """Discover our ancestors and collect their spawnargs. 
            Make an inheritance list then work down it
            so spawnargs get overridden right, finally adding the our own."""
            self.parents = self._findParents(defs)
            for p in reversed(self.parents):
                self.inherited.update(p.spawnargs)
                p.children.add(self)
     
    def getFileDefs(fname, text):
        """Return a dict: { name : entityDef }"""
        text = stripComments(text)
        entdefs = ENT_PTN.findall(text) # [ ( deftext, name ), ... ]
        defs = {}
        for deftext, name in entdefs:
            spawnargs = {}
            sp_text = ARGS_PTN.search(deftext).group(1)
            #debugprint(sp_text)
            strings = SPAWNARG_PTN.findall(sp_text)
            #debugprint('%s Strings found: %d' % (name, len(strings)))
            for idx in range(0, len(strings), 2):
                spawnargs[strings[idx].strip('"').lower()] = strings[idx+1].lower().strip('"')
            parentname = spawnargs.get('inherit')
            defs[name] = entityDef(name, fname, parentname, spawnargs)
        return defs
     
    def getDefs(path, exclude_fms=False):
        """Compile a dict of entity defs from path: { name : entityDef }"""
        debugprint('Checking path ' + path)
        defs = {}   # { name : entityDef }
        for fname, text in findTDMFiles( [path] , exts=['.def']):
            if exclude_fms and '\\fms\\' in fname:
                pass
            else:
                debugprint('Checking file ' + fname)
                defs.update(getFileDefs(fname, text))
                #debugprint('Def count %d' % len(defs))
        # Now we have all defs, populate ancestor details and inherited spawnargs
        for name, e in defs.items():
            e.setParents(defs)
        return defs
     
    if __name__ == '__main__':
        defs = getDefs(GAMEPATH, exclude_fms=True)
        filecount = len( set( d.file for d in defs.values() ) )
        print "Found %d files with %d defs" % (filecount, len(defs))
        # Print out a csv table of AI (anything inheriting from atdm:ai_base
        results = ['"Name","Spawnarg","Default value"']
        ai_base = defs["atdm:ai_base"]
        for ent in ai_base.children:
            sp = ent.spawnargs
            for s in sp.keys():
                results.append('"'+ent.name+'","'+s+'","'+sp[s]+'"')
        for row in results:
            print row
        outfile = file('ai_spawnargs.csv', 'w')
        outfile.writelines(r + '\n' for r in results)
        outfile.close()
    

     

     

  11. Thanks. I did try that of course but for whatever reason I didn't get the error. I was running in a debug compile with a break point set at the point where the zero width and height cause the error to be spat out. When I get home later I'll search for that message and try to work it out.

  12. Cool, I never thought to find out what it meant. That's a good name for an invisible sealant.

     

    Caulk is good for performance because it isn't rendered at all by the engine, it generates no triangles to be drawn. Our "caulk sky" and fog tricks conspire to make it look like caulk surfaces are somehow drawn, which is confusing, but in fact neither is drawn on the caulk surface, which doesn't exist as far as the renderer is concerned. In both cases you're seeing an image in the background, not the caulk itself.

    • Like 2
  13. I've tried out Agent Jones' fix and it works pretty well. It can be unresponsive while the game is loading a map -- it can take a few seconds -- but that's because it relies on internal game code to minimize the game window when it detects that the window is no longer active. Still a massive improvement on having to use the task manager.

     

    I've not been able to reproduce the CropRenderSize error though. I've tried loading several maps while switching in and out, and I've tried leaving TDM in the background to complete the load. No errors yet. I'd like to plug that gap before committing it. Has anyone found a reliable way to make the game crash using the script in the OP?

  14. Just add the "translucent" keyword to the global section of a material, and then add bump and diffuse stages (required), and a specular stage (optional).

    There's a limitation: your material will be drawn as an additive blend over the top of the background, so it will only brighten. Black parts of the texture (diffuse map) will add nothing and not be visible at all. Those are your transparent regions.

    You could possibly use a filtering blend before your lit stage to darken the background. That might be possible by manipulating the same diffuse map using a combination of invertColor(makeIntensity(<diffusemap>)), and I suspect the scale() function could be embedded too to completely blacken out the background where the decal has any colour. It's not complete freedom, but there's a fair amount of flexibility there.

     

    EDIT: I haven't tested the last point. A filtering blend will work only if the engine draws the layers in that order. It just occurred to me that light interactions probably get drawn before any blend stages no matter which order you put them in the material, which would mess things up for darkening the background.

×
×
  • Create New...