def make_tile(vmf: VMF, p1: Vec, p2: Vec, top_mat, bottom_mat, beam_mat): """Generate a 2 or 1 unit thick squarebeams tile. """ prism = vmf.make_prism(p1, p2) brush, t, b, n, s, e, w = prism t.mat = top_mat b.mat = bottom_mat n.mat = beam_mat s.mat = beam_mat e.mat = beam_mat w.mat = beam_mat thickness = abs(p1.z - p2.z) if thickness == 2: # The z-axis texture offset needed # The texture is 512 high, so wrap around # 56 is the offset for the thin-line part of squarebeams # Textures are at 0.25 size, so 4 per hammer unit z_off = ((max(p1.z, p2.z) * 4) + 56) % 512 elif thickness == 1: # Slightly different offset, so the line is still centered z_off = ((max(p1.z, p2.z) * 4) + 54) % 512 else: raise ValueError('Tile has incorrect thickness ' '(expected 1 or 2, got {})'.format(thickness)) n.uaxis = UVAxis(0, 0, 1, offset=z_off) n.vaxis = UVAxis(1, 0, 0, offset=0) s.uaxis = n.uaxis.copy() s.vaxis = n.vaxis.copy() e.uaxis = UVAxis(0, 0, 1, offset=z_off) e.vaxis = UVAxis(0, 1, 0, offset=0) w.uaxis = e.uaxis.copy() w.vaxis = e.vaxis.copy() # Ensure the squarebeams textures aren't replaced, as well as floor tex vbsp.IGNORED_FACES.update(brush.sides) return prism
def make_tile(vmf: VMF, p1: Vec, p2: Vec, top_mat, bottom_mat, beam_mat): """Generate a 2 or 1 unit thick squarebeams tile. """ prism = vmf.make_prism(p1, p2) prism.top.mat = top_mat prism.bottom.mat = bottom_mat prism.north.mat = beam_mat prism.south.mat = beam_mat prism.east.mat = beam_mat prism.west.mat = beam_mat thickness = abs(p1.z - p2.z) if thickness == 2: # The z-axis texture offset needed # The texture is 512 high, so wrap around # 56 is the offset for the thin-line part of squarebeams # Textures are at 0.25 size, so 4 per hammer unit z_off = ((max(p1.z, p2.z) * 4) + 56) % 512 elif thickness == 1: # Slightly different offset, so the line is still centered z_off = ((max(p1.z, p2.z) * 4) + 54) % 512 else: raise ValueError('Tile has incorrect thickness ' '(expected 1 or 2, got {})'.format(thickness)) prism.north.uaxis = UVAxis(0, 0, 1, offset=z_off) prism.north.vaxis = UVAxis(1, 0, 0, offset=0) prism.south.uaxis = prism.north.uaxis.copy() prism.south.vaxis = prism.north.vaxis.copy() prism.east.uaxis = UVAxis(0, 0, 1, offset=z_off) prism.east.vaxis = UVAxis(0, 1, 0, offset=0) prism.west.uaxis = prism.east.uaxis.copy() prism.west.vaxis = prism.east.vaxis.copy() return prism
def parse(cls, ent: Entity): """Parse a template from a config entity. This should be a 'bee2_template_scaling' entity. """ axes = {} for norm, name in ( ((0, 0, 1), 'up'), ((0, 0, -1), 'dn'), ((0, 1, 0), 'n'), ((0, -1, 0), 's'), ((1, 0, 0), 'e'), ((-1, 0, 0), 'w'), ): axes[norm] = ( ent[name + '_tex'], UVAxis.parse(ent[name + '_uaxis']), UVAxis.parse(ent[name + '_vaxis']), srctools.conv_float(ent[name + '_rotation']), ) return cls(ent['template_id'], axes)
def retexture_template( template_data: ExportedTemplate, origin: Vec, fixup: srctools.vmf.EntityFixup=None, replace_tex: dict= srctools.EmptyMapping, force_colour: MAT_TYPES=None, force_grid: str=None, use_bullseye=False, no_clumping=False, ): """Retexture a template at the given location. - Only textures in the TEMPLATE_RETEXTURE dict will be replaced. - Others will be ignored (nodraw, plasticwall, etc) - Wall textures pointing up and down will switch to floor/ceiling textures. - Textures of the same type, normal and inst origin will randomise to the same type. - replace_tex is a replacement table. This overrides everything else. The values should either be a list (random), or a single value. - If force_colour is set, all tile textures will be switched accordingly. If set to 'INVERT', white and black textures will be swapped. - If force_grid is set, all tile textures will be that size: ('wall', '2x2', '4x4', 'special') - If use_bullseye is true, the bullseye textures will be used for all panel sides instead of the normal textures. (This overrides force_grid.) - Fixup is the inst.fixup value, used to allow $replace in replace_tex. - Set no_clump if the brush is used on a special entity, and therefore won't get retextured by the main code. That means we need to directly retexture here. """ import vbsp template = template_data.template # type: Template rev_id_mapping = { new_id: str(old_id) for old_id, new_id in template_data.orig_ids.items() } all_brushes = list(template_data.world) # type: List[Solid] if template_data.detail is not None: all_brushes.extend(template_data.detail.solids) # Template faces are randomised per block and side. This means # multiple templates in the same block get the same texture, so they # can clip into each other without looking bad. rand_prefix = 'TEMPLATE_{}_{}_{}:'.format(*(origin // 128)) # Even if not axis-aligned, make mostly-flat surfaces # floor/ceiling (+-40 degrees) # sin(40) = ~0.707 floor_tolerance = 0.8 can_clump = vbsp.can_clump() # Ensure all values are lists. replace_tex = { key.casefold(): ([value] if isinstance(value, str) else value) for key, value in replace_tex.items() } # For each face, if it needs to be forced to a colour, or None if not. force_colour_face = defaultdict(lambda: None) # Already sorted by priority. for color_picker in template.color_pickers: picker_pos = color_picker.offset.copy().rotate(*template_data.angles) picker_pos += template_data.origin picker_norm = color_picker.normal.copy().rotate(*template_data.angles) if color_picker.grid_snap: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not picker_norm[axis]: picker_pos[axis] = picker_pos[axis] // 128 * 128 + 64 brush = conditions.SOLIDS.get(picker_pos.as_tuple(), None) if brush is None or abs(brush.normal) != abs(picker_norm): # Doesn't exist. continue if color_picker.remove_brush and brush.solid in vbsp.VMF.brushes: brush.solid.remove() for side in color_picker.sides: # Only do the highest priority successful one. if force_colour_face[side] is None: force_colour_face[side] = brush.color for brush in all_brushes: for face in brush: orig_id = rev_id_mapping[face.id] if orig_id in template.skip_faces: continue folded_mat = face.mat.casefold() norm = face.normal() random.seed(rand_prefix + norm.join('_')) if orig_id in template.realign_faces: try: uaxis, vaxis = REALIGN_UVS[norm.as_tuple()] except KeyError: LOGGER.warning( 'Realign face in template "{}" ({} in final) is ' 'not on grid!', template.id, face.id, ) else: face.uaxis = uaxis.copy() face.vaxis = vaxis.copy() try: override_mat = replace_tex['#' + orig_id] except KeyError: try: override_mat = replace_tex[folded_mat] except KeyError: override_mat = None if override_mat is not None: # Replace_tex overrides everything. mat = random.choice(override_mat) if mat[:1] == '$' and fixup is not None: mat = fixup[mat] if mat.startswith('<') or mat.endswith('>'): # Lookup in the style data. mat = vbsp.get_tex(mat[1:-1]) face.mat = mat continue try: tex_type = TEMPLATE_RETEXTURE[folded_mat] except KeyError: continue # It's nodraw, or something we shouldn't change if isinstance(tex_type, str): # It's something like squarebeams or backpanels, just look # it up face.mat = vbsp.get_tex(tex_type) if tex_type == 'special.goo_cheap': if norm != (0, 0, 1): # Goo must be facing upright! # Retexture to nodraw, so a template can be made with # all faces goo to work in multiple orientations. face.mat = 'tools/toolsnodraw' else: # Goo always has the same orientation! face.uaxis = UVAxis( 1, 0, 0, offset=0, scale=vbsp_options.get(float, 'goo_scale'), ) face.vaxis = UVAxis( 0, -1, 0, offset=0, scale=vbsp_options.get(float, 'goo_scale'), ) continue # It's a regular wall type! tex_colour, grid_size = tex_type if force_colour_face[orig_id] is not None: tex_colour = force_colour_face[orig_id] elif force_colour == 'INVERT': # Invert the texture tex_colour = ( MAT_TYPES.white if tex_colour is MAT_TYPES.black else MAT_TYPES.black ) elif force_colour is not None: tex_colour = force_colour if force_grid is not None: grid_size = force_grid if 1 in norm or -1 in norm: # Facing NSEW or up/down # If axis-aligned, make the orientation aligned to world # That way multiple items merge well, and walls are upright. # We allow offsets < 1 grid tile, so items can be offset. face.uaxis.offset %= TEMP_TILE_PIX_SIZE[grid_size] face.vaxis.offset %= TEMP_TILE_PIX_SIZE[grid_size] if use_bullseye: # We want to use the bullseye textures, instead of normal # ones if norm.z < -floor_tolerance: face.mat = vbsp.get_tex( 'special.bullseye_{}_floor'.format(tex_colour) ) elif norm.z > floor_tolerance: face.mat = vbsp.get_tex( 'special.bullseye_{}_ceiling'.format(tex_colour) ) else: face.mat = '' # Ensure next if statement triggers # If those aren't defined, try the wall texture.. if face.mat == '': face.mat = vbsp.get_tex( 'special.bullseye_{}_wall'.format(tex_colour) ) if face.mat != '': continue # Set to a bullseye texture, # don't use the wall one if grid_size == 'special': # Don't use wall on faces similar to floor/ceiling: if -floor_tolerance < norm.z < floor_tolerance: face.mat = vbsp.get_tex( 'special.{!s}_wall'.format(tex_colour) ) else: face.mat = '' # Ensure next if statement triggers # Various fallbacks if not defined if face.mat == '': face.mat = vbsp.get_tex( 'special.{!s}'.format(tex_colour) ) if face.mat == '': # No special texture - use a wall one. grid_size = 'wall' else: # Set to a special texture, continue # don't use the wall one if norm.z > floor_tolerance: grid_size = 'ceiling' if norm.z < -floor_tolerance: grid_size = 'floor' if can_clump and not no_clumping: # For the clumping algorithm, set to Valve PeTI and let # clumping handle retexturing. vbsp.IGNORED_FACES.remove(face) if tex_colour is MAT_TYPES.white: if grid_size == '4x4': face.mat = 'tile/white_wall_tile003f' elif grid_size == '2x2': face.mat = 'tile/white_wall_tile003c' else: face.mat = 'tile/white_wall_tile003h' elif tex_colour is MAT_TYPES.black: if grid_size == '4x4': face.mat = 'metal/black_wall_metal_002b' elif grid_size == '2x2': face.mat = 'metal/black_wall_metal_002a' else: face.mat = 'metal/black_wall_metal_002e' else: face.mat = vbsp.get_tex( '{!s}.{!s}'.format(tex_colour, grid_size) ) for over in template_data.overlay[:]: random.seed('TEMP_OVERLAY_' + over['basisorigin']) mat = over['material'].casefold() if mat in replace_tex: mat = random.choice(replace_tex[mat]) if mat[:1] == '$': mat = fixup[mat] if mat.startswith('<') or mat.endswith('>'): # Lookup in the style data. mat = vbsp.get_tex(mat[1:-1]) elif mat in vbsp.TEX_VALVE: mat = vbsp.get_tex(vbsp.TEX_VALVE[mat]) else: continue if mat == '': # If blank, remove the overlay from the map and the list. # (Since it's inplace, this can affect the tuple.) template_data.overlay.remove(over) over.remove() else: over['material'] = mat
def retexture_template( template_data: ExportedTemplate, origin: Vec, fixup: EntityFixup = None, replace_tex: Mapping[str, Union[List[str], str]] = srctools.EmptyMapping, force_colour: Portalable = None, force_grid: TileSize = None, generator: GenCat = GenCat.NORMAL, sense_offset: Optional[Vec] = None, ): """Retexture a template at the given location. - Only textures in the TEMPLATE_RETEXTURE dict will be replaced. - Others will be ignored (nodraw, plasticwall, etc) - Wall textures pointing up and down will switch to floor/ceiling textures. - Textures of the same type, normal and inst origin will randomise to the same type. - replace_tex is a replacement table. This overrides everything else. The values should either be a list (random), or a single value. - If force_colour is set, all tile textures will be switched accordingly. If set to 'INVERT', white and black textures will be swapped. - If force_grid is set, all tile textures will be that size. - generator defines the generator category to use for surfaces. - Fixup is the inst.fixup value, used to allow $replace in replace_tex. - If sense_offset is set, color pickers and tilesetters will be treated as if they were locally offset this far in the template. """ template = template_data.template rev_id_mapping = { new_id: str(old_id) for old_id, new_id in template_data.orig_ids.items() } all_brushes = list(template_data.world) if template_data.detail is not None: all_brushes.extend(template_data.detail.solids) # Template faces are randomised per block and side. This means # multiple templates in the same block get the same texture, so they # can clip into each other without looking bad. rand_prefix = 'TEMPLATE_{0.x}_{0.y}_{0.z}:'.format(origin // 128) # Reprocess the replace_tex passed in, converting values. evalled_replace_tex: Dict[str, List[str]] = {} for key, value in replace_tex.items(): if isinstance(value, str): value = [value] if fixup is not None: # Convert the material and key for fixup names. value = [ fixup[mat] if mat.startswith('$') else mat for mat in value ] if key.startswith('$'): key = fixup[key] # If starting with '#', it's a face id, or a list of those. if key.startswith('#'): for k in key[1:].split(): try: old_id = int(k) except (ValueError, TypeError): pass else: evalled_replace_tex.setdefault('#' + str(old_id), []).extend(value) else: evalled_replace_tex.setdefault(key.casefold(), []).extend(value) if sense_offset is None: sense_offset = Vec() else: sense_offset = sense_offset.copy().rotate(*template_data.angles) # For each face, if it needs to be forced to a colour, or None if not. # If a string it's forced to that string specifically. force_colour_face: Dict[str, Union[Portalable, str, None]] = defaultdict(lambda: None) # Picker names to their results. picker_results: Dict[str, Optional[Portalable]] = template_data.picker_results picker_type_results: Dict[str, Optional[TileType]] = {} # If the "use patterns" option is enabled, face ID -> temp face to copy from. picker_patterned: Dict[str, Optional[Side]] = defaultdict(lambda: None) # Then also a cache of the tiledef -> dict of template faces. pattern_cache: Dict[tiling.TileDef, Dict[Tuple[int, int], Side]] = {} # Already sorted by priority. for color_picker in template.color_pickers: if not color_picker.visgroups.issubset(template_data.visgroups): continue picker_pos = color_picker.offset.copy().rotate(*template_data.angles) picker_pos += template_data.origin + sense_offset picker_norm = Vec(color_picker.normal).rotate(*template_data.angles) if color_picker.grid_snap: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not picker_norm[axis]: picker_pos[axis] = picker_pos[axis] // 128 * 128 + 64 try: tiledef, u, v = tiling.find_tile(picker_pos, picker_norm) except KeyError: # Doesn't exist. But only set if not already present. if color_picker.name: picker_results.setdefault(color_picker.name, None) picker_type_results.setdefault(color_picker.name, None) continue tile_type = tiledef[u, v] picker_type_results[color_picker.name] = tile_type try: tile_color = tile_type.color except ValueError: # Not a tile with color (void, etc). Treat as missing a color. picker_results.setdefault(color_picker.name, None) continue if color_picker.name and picker_results.get(color_picker.name, None) is None: picker_results[color_picker.name] = tile_color if color_picker.use_pattern: # Generate the brushwork for the tile to determine the top faces # required. We can then throw away the brushes themselves. try: pattern = pattern_cache[tiledef] except KeyError: pattern = pattern_cache[tiledef] = {} tiledef.gen_multitile_pattern( VMF(), {(u, v): tiledef[u, v] for u in (0, 1, 2, 3) for v in (0, 1, 2, 3)}, is_wall=tiledef.normal.z != 0, bevels=(False, False, False, False), normal=tiledef.normal, face_output=pattern, ) for side in color_picker.sides: if picker_patterned[side] is None and (u, v) in pattern: picker_patterned[side] = pattern[u, v] else: # Only do the highest priority successful one. for side in color_picker.sides: if force_colour_face[side] is None: if tile_color is tile_color.WHITE: force_colour_face[ side] = color_picker.force_tex_white or tile_color else: force_colour_face[ side] = color_picker.force_tex_black or tile_color if color_picker.after is AfterPickMode.VOID: tiledef[u, v] = TileType.VOID elif color_picker.after is AfterPickMode.NODRAW: tiledef[u, v] = TileType.NODRAW for tile_setter in template.tile_setters: if not tile_setter.visgroups.issubset(template_data.visgroups): continue setter_pos = Vec(tile_setter.offset).rotate(*template_data.angles) setter_pos += template_data.origin + sense_offset setter_norm = Vec(tile_setter.normal).rotate(*template_data.angles) setter_type = tile_setter.tile_type # type: TileType if tile_setter.color == 'copy': if not tile_setter.picker_name: raise ValueError('"{}": Tile Setter set to copy mode ' 'must have a color picker!'.format( template.id)) # If a color picker is set, it overrides everything else. try: setter_type = picker_type_results[tile_setter.picker_name] except KeyError: raise ValueError('"{}": Tile Setter specified color picker ' '"{}" which does not exist!'.format( template.id, tile_setter.picker_name)) if setter_type is None: raise ValueError( '"{}": Color picker "{}" has no tile to pick!'.format( template.id, tile_setter.picker_name)) elif setter_type.is_tile: if tile_setter.picker_name: # If a color picker is set, it overrides everything else. try: setter_color = picker_results[tile_setter.picker_name] except KeyError: raise ValueError( '"{}": Tile Setter specified color picker ' '"{}" which does not exist!'.format( template.id, tile_setter.picker_name)) if setter_color is None: raise ValueError( '"{}": Color picker "{}" has no tile to pick!'.format( template.id, tile_setter.picker_name)) elif isinstance(tile_setter.color, Portalable): # The color was specifically set. setter_color = tile_setter.color elif isinstance(force_colour, Portalable): # Otherwise it copies the forced colour - # it needs to be white or black. setter_color = force_colour else: # We need a forced color, but none was provided. raise ValueError( '"{}": Tile Setter set to use colour value from the ' "template's overall color, " 'but not given one!'.format(template.id)) # Inverting applies to all of these. if force_colour == 'INVERT': setter_color = ~setter_color setter_type = TileType.with_color_and_size( setter_type.tile_size, setter_color, ) tiling.edit_quarter_tile( setter_pos, setter_norm, setter_type, silent=True, # Don't log missing positions. force=tile_setter.force, ) for brush in all_brushes: for face in brush: orig_id = rev_id_mapping[face.id] if orig_id in template.skip_faces: continue template_face = picker_patterned[orig_id] if template_face is not None: face.mat = template_face.mat face.uaxis = template_face.uaxis.copy() face.vaxis = template_face.vaxis.copy() continue folded_mat = face.mat.casefold() norm = face.normal() random.seed(rand_prefix + norm.join('_')) if orig_id in template.realign_faces: try: uaxis, vaxis = REALIGN_UVS[norm.as_tuple()] except KeyError: LOGGER.warning( 'Realign face in template "{}" ({} in final) is ' 'not on grid!', template.id, face.id, ) else: face.uaxis = uaxis.copy() face.vaxis = vaxis.copy() override_mat: Optional[List[str]] try: override_mat = evalled_replace_tex['#' + orig_id] except KeyError: try: override_mat = evalled_replace_tex[folded_mat] except KeyError: override_mat = None if override_mat is not None: # Replace_tex overrides everything. mat = random.choice(override_mat) if mat[:1] == '$' and fixup is not None: mat = fixup[mat] if mat.startswith('<') and mat.endswith('>'): # Lookup in the style data. gen, mat = texturing.parse_name(mat[1:-1]) mat = gen.get(face.get_origin(), mat) # If blank, don't set. if mat: face.mat = mat continue if folded_mat == 'tile/white_wall_tile003b': LOGGER.warning( '"{}": white_wall_tile003b has changed definitions.', template.id) try: tex_type = TEMPLATE_RETEXTURE[folded_mat] except KeyError: continue # It's nodraw, or something we shouldn't change tex_colour: Optional[Portalable] gen_type, tex_name, tex_colour = tex_type if not gen_type.is_tile: # It's something like squarebeams or backpanels, so # we don't need much special handling. texturing.apply(gen_type, face, tex_name) if tex_name in ('goo', 'goo_cheap'): if norm != (0, 0, 1): # Goo must be facing upright! # Retexture to nodraw, so a template can be made with # all faces goo to work in multiple orientations. face.mat = 'tools/toolsnodraw' else: # Goo always has the same orientation! face.uaxis = UVAxis( 1, 0, 0, offset=0, scale=vbsp_options.get(float, 'goo_scale'), ) face.vaxis = UVAxis( 0, -1, 0, offset=0, scale=vbsp_options.get(float, 'goo_scale'), ) continue else: assert isinstance(tex_colour, Portalable) # Allow overriding to panel or bullseye types. if gen_type is GenCat.NORMAL: gen_type = generator # Otherwise, it's a panel wall or the like if force_colour_face[orig_id] is not None: tex_colour = force_colour_face[orig_id] if isinstance(tex_colour, str): face.mat = tex_colour continue elif force_colour == 'INVERT': # Invert the texture tex_colour = ~tex_colour elif force_colour is not None: tex_colour = force_colour if force_grid is not None: tex_name = force_grid texturing.apply(gen_type, face, tex_name, tex_colour) for over in template_data.overlay[:]: over_pos = Vec.from_str(over['basisorigin']) mat = over['material'].casefold() if mat in replace_tex: mat = random.choice(replace_tex[mat]) if mat[:1] == '$' and fixup is not None: mat = fixup[mat] if mat.startswith('<') or mat.endswith('>'): mat = mat[1:-1] gen, tex_name = texturing.parse_name(mat[1:-1]) mat = gen.get(over_pos, tex_name) else: try: sign_type = consts.Signage(mat) except ValueError: pass else: mat = texturing.OVERLAYS.get(over_pos, sign_type) if mat == '': # If blank, remove the overlay from the map and the list. # (Since it's inplace, this can affect the tuple.) template_data.overlay.remove(over) over.remove() else: over['material'] = mat