Adjust the settings to your needs and export the track.
Step 2 – Importing into UE5
I already have setup up a basic vehicle game template I am using, if you don’t have any you can use the one shipped with the unreal engine or google up some tutorials, there are plenty.
First a new level should be created, I suggest to select “Open World” or “Basic” – I used “basic” for the following steps.
First of the floor static mesh (SM) can be removed as it is not needed.
Second a new folder, according to your track, should be created – I chose “industrial”
Navigate inside that newly created folder and click on “Import” right above the content browser
Navigate to the folder where you exported the .fbx-file to and select the correct .fbx-file, then hit “open”
In the following dialog window make sure you have selected “Create New Materials” within the “Material Import Method” so that the engine is creating the materials for you. Then simply hit “import” and sit back and wait.
This may take a while depending on how many objects your imported track has. Once its done you will find the imported geometry (static meshes), textures and materials in your content browser
First, now you should hit the “Save all” button to save the level and all imported assets, that will make alot of perfomance difference.
Once the saving process is done you surely want to have the track in your level, right? Okay, gotcha.
The easiest way is to apply a filter to your working folder, filtering for “static meshes” because within the whole texture-material-static-mesh-mess you simply don’t want to search for the SMs right?
Step 2 – Getting the track to play
As mentioned before, set a filter by clicking the filter-icon and selecting “static mesh”.
then apply the filter by simply clicking it on the left hand side (depending on your layout) of the content browser:
And all off a sudden you will see all the beautiful meshes you freshly imported, all textured and ready to use (the view might differ from mine as I sorted, grouped and united the track’s objects before to fit my needs)
Then all you need to do is press CTRL+A (to select all static meshes) and drag&drop it into the viewport above
And you have successfully imported the track to the unreal engine 5.
Step 3 – adjusting the collision
per default you would not be able to “race” the track right now as the collision settings would block you to do so. So the easiest way to adjust this is – as I do know you simply want to RACE THE TRACK – open the static mesh’s properties (one by one, by simply double-clicking it) and go to the collision settings:
In there you will find the setting “Collision Complexity” – just set this to “use Complex Collision as simple”
This will force the engine to use the mesh of the static mesh as a collision instead of creating a basic box around it – to make the difference clear on a smaller mesh you will see both, simple and complex collision on the following mesh:
green being the simple collision, turquise being the complex collision. In this case using simple collision would lead to not being able to drive through the crane.
Make sure to hit the save button as changing the collision preset is not saved automatically!
Repeat this step for all needed objects (depending on how many you have)
Step 4 – adjusting the size
This step can be very frustrating, but is mandatory. You need to get any reference point from the old game and transfer it to the new one to adjust the sizes – more or less, depending on how accurate you want it to be.
As I do not have yet a Porsche model ingame I am using the existing McLaren F1 as reference as well as some ingame shots from the original game.
First we take a screenshot of the starting lane and find the same spot in the unreal engine project
Once we found the spot we add our car as reference object
Now, for my good, it seems to be pretty well scaled. If you are not the lucky one to find the scaling right, just select all track objects in the level outliner (top right in the default layout) and adjust the scaling accordingly – attention, some math can be required (e.g. the original game screenshot car (911 GT-1 ) is 1990mm width, McLaren F1 is 1820mm width), so you could measure the actual ingame road lane width and transfer it to unreal engine using simple math.
Step 4 – adjusting materials
Gladly we have all needed materials ready, sadly they are all missing the alpha channel and, as the NFS5-Engine is dealing a lot of objects as two-sided, this setting is also missing and needs to be adjusted.
This is how to do it
First, detect the materials/textures that have an alpha channel and/or are two sided, e.g. signs, trees etc.
Once you find the texture/material, double click the material (NOT the texture). The editor will open a new window, looking like this
In here, left hand side, change the “Blend mode” to “Masked” and check “Two Sided”
Then in the notes view you can now drag the A (for alpha) channel note to the opacity mask note
You will notice (depending on the texture) that there now will be transparent parts and the shadow will be calculated accordingly
Afterwards always hit APPLY first, then SAVE
repeat this step for all needed textures/materials. Knowing the original track/textures does help as well as flying through the track in the engine.
Step 5 – adjusting lightning
Of couse you could race now, but as you are tend to get the atmosphere right we should adjust the lightning and atmosphere – using – again – old ingame references.
In this *new* tutorial I explain how everyone of you can easily import tracks from NFS4 into blender. Forget the old tutorial. After digging the internet I found a small set of tools that will make your life much easier.
The old way as described in this tutorial was a huge workflow with alot of conversion time needed for many many manual processes such as converting textures and geometry, correcting texture offsets etc.
While I was – more or less accidently – searching for a method to convert NFS5 Porsche Unleashed tracks into blender I found the toolset “speedtools” by Rafał Kuźnia.
That guy is a time saver.
Once you worked through all the setup process for the tools you can just one-click import NFS4-tracks into blender, that’s it – just as easy as opening a .fbx-file.
It’s true, you can just go to file > import > track resources and open any* NFS4-track
Download all of them and have the NFS4-track files ready.
Install Blender
obviously, Blender needs to be installed, sherlock.
Install Kaitai Struct compiler
Right after you installed Blender go ahead and download and install the Katai Struct compiler. I suggest to use the install’s default settings and quickly click through the installer.
Start Blender
Now it’s time to start Blender. When blender has opened up you want to head to the scripting section from the very top main menu (at the very right side):
The viewport will change a lot. The window you are looking for is the bottom left console window where the input has the leading three “>” characters:
In this window just copy-paste the following command end hit enter. Once complete there will be a prompt with “0”. This command will install the python-tools needed.
Once this step is complete you need to save the blank project. Bear in mind that the project’s save location will be used from the importer to storage the imported images and assets in subfolder(s) created within the save location, so I suggest to choose carefully your save location, like the following:
../projects/hs/tracks/hometown/hometown.blend
Time to import
Yet, as all the steps above are done, all is set up for the first NFSHS-track import to blender.
To enable the easy import mode, you need to copy the content of the following script to your blender scripting tab, or save the file somewhere in your file system and open it up in blender:
#
# Copyright (c) 2023 Rafał Kuźnia <rafal.kuznia@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
from __future__ import annotations
import logging
from abc import ABCMeta, abstractmethod
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from functools import total_ordering
from itertools import chain, groupby
from math import pi
from pathlib import Path
from typing import Any
import bpy
import mathutils
from bpy.props import BoolProperty, EnumProperty, StringProperty
from more_itertools import collapse, duplicates_everseen, one, unique_everseen
from speedtools import TrackData, VivData
from speedtools.types import (
Action,
AnimationAction,
BaseMesh,
BlendMode,
Camera,
DirectionalLight,
DrawableMesh,
Light,
Matrix3x3,
Part,
Polygon,
Resource,
ShapeKey,
Vector3d,
Vertex,
)
from speedtools.utils import export_resource
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
bl_info = {
"name": "Import NFS4 Track",
"author": "Rafał Kuźnia",
"version": (1, 0, 0),
"blender": (3, 4, 1),
"location": "File > Import > Track resource",
"description": "Imports a NFS4 track files (meshes, textures and objects)."
"Scripts/Import-Export/Track_Resource",
"category": "Import-Export",
}
@total_ordering
@dataclass(frozen=True)
class ExtendedResource:
resource: Resource
backface_culling: bool
def __lt__(self, other: ExtendedResource) -> bool:
return hash(self) < hash(other)
class BaseImporter(metaclass=ABCMeta):
def __init__(self, material_map: Callable[[Polygon], Resource]) -> None:
self.materials: dict[ExtendedResource, bpy.types.Material] = {}
self.material_map = material_map
@classmethod
def duplicate_common_vertices(cls, mesh: DrawableMesh) -> DrawableMesh:
unique_vert_polys = list(unique_everseen(mesh.polygons, key=lambda x: frozenset(x.face)))
duplicate_vert_polys = list(
duplicates_everseen(mesh.polygons, key=lambda x: frozenset(x.face))
)
faces = frozenset(chain.from_iterable(poly.face for poly in duplicate_vert_polys))
verts_to_duplicate = [mesh.vertices[x] for x in faces]
mapping = {f: i for i, f in enumerate(faces, start=len(mesh.vertices))}
def _make_polygon(polygon: Polygon) -> Polygon:
face = tuple(mapping[f] for f in polygon.face)
return Polygon(
face=face,
uv=polygon.uv,
material=polygon.material,
backface_culling=polygon.backface_culling,
)
polygons = unique_vert_polys + [_make_polygon(polygon) for polygon in duplicate_vert_polys]
vertices = list(mesh.vertices) + verts_to_duplicate
return DrawableMesh(vertices=vertices, polygons=polygons)
def _extender_resource_map(self, polygon: Polygon) -> ExtendedResource:
resource = self.material_map(polygon)
return ExtendedResource(resource=resource, backface_culling=polygon.backface_culling)
def _link_texture_to_shader(
self, node_tree: bpy.types.NodeTree, texture: bpy.types.Node, shader: bpy.types.Node
) -> None:
node_tree.links.new(texture.outputs["Color"], shader.inputs["Base Color"])
node_tree.links.new(texture.outputs["Alpha"], shader.inputs["Alpha"])
def _set_blend_mode(
self,
node_tree: bpy.types.NodeTree,
shader_output: bpy.types.NodeSocket,
bpy_material: bpy.types.Material,
resource: Resource,
) -> bpy.types.NodeSocket:
if resource.blend_mode is BlendMode.ALPHA:
bpy_material.blend_method = "BLEND"
elif resource.blend_mode is BlendMode.ADDITIVE:
bpy_material["SPT_additive"] = True
else:
bpy_material.alpha_threshold = 0.001
bpy_material.blend_method = "CLIP"
return shader_output
def _make_material(self, ext_resource: ExtendedResource) -> bpy.types.Material:
resource = ext_resource.resource
images_dir = Path(bpy.path.abspath("//images"))
export_resource(resource, directory=images_dir)
bpy_material = bpy.data.materials.new(resource.name)
bpy_material.use_nodes = True
image_path = Path(images_dir, f"{resource.name}.png")
image = bpy.data.images.load(str(image_path), check_existing=True)
node_tree = bpy_material.node_tree
material_output = node_tree.nodes.get("Material Output")
image_texture = node_tree.nodes.new("ShaderNodeTexImage")
image_texture.image = image # type: ignore[attr-defined]
image_texture.extension = "EXTEND" # type: ignore[attr-defined]
bsdf = node_tree.nodes["Principled BSDF"]
bsdf.inputs["Specular"].default_value = 0 # type: ignore[attr-defined]
bsdf.inputs["Roughness"].default_value = 1 # type: ignore[attr-defined]
bsdf.inputs["Sheen Tint"].default_value = 0 # type: ignore[attr-defined]
self._link_texture_to_shader(node_tree=node_tree, texture=image_texture, shader=bsdf)
output_socket = self._set_blend_mode(
node_tree=node_tree,
shader_output=bsdf.outputs["BSDF"],
bpy_material=bpy_material,
resource=resource,
)
node_tree.links.new(output_socket, material_output.inputs["Surface"])
bpy_material.use_backface_culling = ext_resource.backface_culling
return bpy_material
def _map_material(self, ext_resource: ExtendedResource) -> bpy.types.Material:
try:
return self.materials[ext_resource]
except KeyError:
bpy_material = self._make_material(ext_resource=ext_resource)
self.materials[ext_resource] = bpy_material
return self.materials[ext_resource]
def make_base_mesh(self, name: str, mesh: BaseMesh) -> bpy.types.Mesh:
bpy_mesh = bpy.data.meshes.new(name)
bpy_mesh.from_pydata(
vertices=list(mesh.vertex_locations),
edges=[],
faces=[polygon.face for polygon in mesh.polygons],
)
return bpy_mesh
def set_object_location(self, obj: bpy.types.Object, location: Vector3d) -> None:
mu_location = mathutils.Vector(location)
obj.location = mu_location
def set_object_action(self, obj: bpy.types.Object, action: AnimationAction) -> None:
animation = action.animation
obj.rotation_mode = "QUATERNION"
if obj.animation_data is None:
anim_data = obj.animation_data_create()
else:
anim_data = obj.animation_data
bpy_action = bpy.data.actions.new(name=str(action.action))
anim_data.action = bpy_action
for index, (location, quaternion) in enumerate(
zip(animation.locations, animation.quaternions)
):
mu_location = mathutils.Vector(location)
mu_quaternion = mathutils.Quaternion(quaternion)
mu_quaternion = mu_quaternion.normalized()
mu_quaternion = mu_quaternion.inverted()
obj.delta_location = mu_location
obj.delta_rotation_quaternion = mu_quaternion # type: ignore[assignment]
interval = index * animation.delay
obj.keyframe_insert(
data_path="delta_location", frame=interval, options={"INSERTKEY_CYCLE_AWARE"}
)
obj.keyframe_insert(
data_path="delta_rotation_quaternion",
frame=interval,
options={"INSERTKEY_CYCLE_AWARE"},
)
points = chain.from_iterable(fcurve.keyframe_points for fcurve in bpy_action.fcurves)
for point in points:
point.interpolation = "LINEAR"
bpy_action.name = f"{obj.name}-action-{action.action}"
track = anim_data.nla_tracks.new()
track.strips.new(name=bpy_action.name, start=0, action=bpy_action)
def set_object_rotation(
self,
obj: bpy.types.Object,
transform: Matrix3x3,
offset: mathutils.Euler | None = None,
) -> None:
mu_matrix = mathutils.Matrix(transform)
if offset:
mu_euler = offset
mu_euler.rotate(mu_matrix.to_euler("XYZ")) # type: ignore # pylint: disable=all
else:
mu_euler = mu_matrix.to_euler("XYZ") # type: ignore # pylint: disable=all
obj.rotation_mode = "XYZ"
obj.rotation_euler = mu_euler # type: ignore[assignment]
def make_drawable_object(
self, name: str, mesh: DrawableMesh, import_shading: bool = False
) -> bpy.types.Object:
bpy_mesh = self.make_base_mesh(name=name, mesh=mesh)
uv_layer = bpy_mesh.uv_layers.new()
uvs = collapse(polygon.uv for polygon in mesh.polygons)
uv_layer.data.foreach_set("uv", list(uvs))
if mesh.vertex_normals:
normals = tuple(mesh.vertex_normals)
# I have no idea if setting the normals even works
bpy_mesh.normals_split_custom_set_from_vertices(normals) # type: ignore[arg-type]
if mesh.vertex_colors and import_shading:
colors = collapse(color.rgba_float for color in mesh.vertex_colors)
bpy_colors = bpy_mesh.color_attributes.new(
name="Shading", type="FLOAT_COLOR", domain="POINT"
)
bpy_colors.data.foreach_set("color", tuple(colors)) # type: ignore[attr-defined]
polygon_pairs = zip(mesh.polygons, bpy_mesh.polygons)
sorted_by_material = sorted(polygon_pairs, key=lambda x: self._extender_resource_map(x[0]))
grouped_by_material = groupby(
sorted_by_material, key=lambda x: self._extender_resource_map(x[0])
)
for index, (key, group) in enumerate(grouped_by_material):
material = self._map_material(key)
bpy_mesh.materials.append(material)
for _, bpy_polygon in group:
bpy_polygon.use_smooth = True
bpy_polygon.material_index = index
bpy_mesh.validate()
bpy_obj = bpy.data.objects.new(name, bpy_mesh)
if mesh.shape_keys:
bpy_obj.shape_key_add(name="Basis")
return bpy_obj
def make_light_object(self, name: str, light: Light) -> bpy.types.Object:
bpy_light = bpy.data.lights.new(name=name, type="POINT")
bpy_light.color = light.attributes.color.rgb_float
bpy_light.use_custom_distance = True
bpy_light.cutoff_distance = 15.0
bpy_light.specular_factor = 0.2
bpy_light.energy = 500 # type: ignore[attr-defined]
bpy_light.use_shadow = False # type: ignore[attr-defined]
bpy_obj = bpy.data.objects.new(name=name, object_data=bpy_light)
self.set_object_location(obj=bpy_obj, location=light.location)
return bpy_obj
def make_directional_light_object(
self, name: str, light: DirectionalLight
) -> bpy.types.Object:
bpy_sun = bpy.data.lights.new(name=name, type="SUN")
bpy_obj = bpy.data.objects.new(name=name, object_data=bpy_sun)
mu_euler = mathutils.Euler(light.euler_xyz)
bpy_obj.rotation_mode = "XYZ"
bpy_obj.rotation_euler = mu_euler # type: ignore[assignment]
return bpy_obj
def make_camera_object(self, name: str, camera: Camera) -> bpy.types.Object:
bpy_camera = bpy.data.cameras.new(name=name)
bpy_obj = bpy.data.objects.new(name=name, object_data=bpy_camera)
offset = mathutils.Euler((pi / 2, 0, 0))
self.set_object_location(obj=bpy_obj, location=camera.location)
self.set_object_rotation(obj=bpy_obj, transform=camera.transform, offset=offset)
return bpy_obj
def make_shape_key(self, obj: bpy.types.Object, shape_key: ShapeKey) -> None:
bpy_shape_key = obj.shape_key_add(name=shape_key.type.name)
bpy_shape_key.interpolation = "KEY_LINEAR"
for data, vertex in zip(bpy_shape_key.data, shape_key.vertices, strict=True):
data.co = vertex.location # type: ignore[attr-defined]
class TrackImportStrategy(metaclass=ABCMeta):
@abstractmethod
def import_track(
self,
track: TrackData,
import_collision: bool = False,
import_shading: bool = False,
import_actions: bool = False,
import_cameras: bool = False,
) -> None:
pass
class TrackImportGLTF(TrackImportStrategy, BaseImporter):
def import_track(
self,
track: TrackData,
import_collision: bool = False,
import_shading: bool = False,
import_actions: bool = False,
import_cameras: bool = False,
) -> None:
bpy.context.scene.render.fps = track.ANIMATION_FPS
track_collection = bpy.data.collections.new("Track segments")
bpy.context.scene.collection.children.link(track_collection)
for index, segment in enumerate(track.track_segments):
name = f"Segment {index}"
segment_collection = bpy.data.collections.new(name=name)
track_collection.children.link(segment_collection)
bpy_obj = self.make_drawable_object(
name=name, mesh=segment.mesh, import_shading=import_shading
)
segment_collection.objects.link(bpy_obj)
if import_collision:
for collision_index, collision_mesh in enumerate(segment.collision_meshes):
effect = collision_mesh.collision_effect
name = f"Collision {collision_index}.{effect}-colonly"
bpy_mesh = self.make_base_mesh(name=name, mesh=collision_mesh)
bpy_obj = bpy.data.objects.new(name, bpy_mesh)
segment_collection.objects.link(bpy_obj)
bpy_obj.hide_set(True)
object_collection = bpy.data.collections.new("Objects")
bpy.context.scene.collection.children.link(object_collection)
for index, obj in enumerate(track.objects):
name = f"Object {index}"
mesh = self.duplicate_common_vertices(mesh=obj.mesh)
bpy_obj = self.make_drawable_object(
name=name, mesh=mesh, import_shading=import_shading
)
actions = (
obj.actions
if import_actions
else filter(lambda x: x.action is Action.DEFAULT_LOOP, obj.actions)
)
for action in actions:
self.set_object_action(obj=bpy_obj, action=action)
if obj.location:
self.set_object_location(obj=bpy_obj, location=obj.location)
if obj.transform:
self.set_object_rotation(obj=bpy_obj, transform=obj.transform)
object_collection.objects.link(bpy_obj)
light_collection = bpy.data.collections.new("Lights")
bpy.context.scene.collection.children.link(light_collection)
for index, light in enumerate(track.lights):
name = f"Light {index}"
bpy_obj = self.make_light_object(name=name, light=light)
light_collection.objects.link(bpy_obj)
directional_light = track.directional_light
if directional_light:
bpy_obj = self.make_directional_light_object(name="sun", light=directional_light)
light_collection.objects.link(bpy_obj)
if import_cameras:
camera_collection = bpy.data.collections.new("Cameras")
bpy.context.scene.collection.children.link(camera_collection)
for index, camera in enumerate(track.cameras):
bpy_obj = self.make_camera_object(name=f"Camera {index}", camera=camera)
camera_collection.objects.link(bpy_obj)
class TrackImportBlender(TrackImportGLTF):
def _link_texture_to_shader(
self, node_tree: bpy.types.NodeTree, texture: bpy.types.Node, shader: bpy.types.Node
) -> None:
color_attributes = node_tree.nodes.new("ShaderNodeAttribute")
color_attributes.attribute_name = "Shading" # type: ignore[attr-defined]
mixer = node_tree.nodes.new("ShaderNodeMixRGB")
mixer.blend_type = "MULTIPLY" # type: ignore[attr-defined]
mixer.inputs["Fac"].default_value = 1.0 # type: ignore[attr-defined]
node_tree.links.new(texture.outputs["Color"], mixer.inputs["Color1"])
node_tree.links.new(color_attributes.outputs["Color"], mixer.inputs["Color2"])
node_tree.links.new(mixer.outputs["Color"], shader.inputs["Base Color"])
node_tree.links.new(texture.outputs["Alpha"], shader.inputs["Alpha"])
def _set_blend_mode(
self,
node_tree: bpy.types.NodeTree,
shader_output: bpy.types.NodeSocket,
bpy_material: bpy.types.Material,
resource: Resource,
) -> bpy.types.NodeSocket:
shader_output = super()._set_blend_mode(
node_tree=node_tree,
shader_output=shader_output,
bpy_material=bpy_material,
resource=resource,
)
output_socket = shader_output
if resource.blend_mode is BlendMode.ADDITIVE:
bpy_material.blend_method = "BLEND"
transparent_bsdf = node_tree.nodes.new("ShaderNodeBsdfTransparent")
add_shader = node_tree.nodes.new("ShaderNodeAddShader")
node_tree.links.new(shader_output, add_shader.inputs[0])
node_tree.links.new(transparent_bsdf.outputs["BSDF"], add_shader.inputs[1])
output_socket = add_shader.outputs["Shader"]
return output_socket
class CarImporterSimple(BaseImporter):
def import_car(self, parts: Iterable[Part]) -> None:
car_collection = bpy.data.collections.new("Car parts")
bpy.context.scene.collection.children.link(car_collection)
for part in parts:
bpy_obj = self.make_drawable_object(name=part.name, mesh=part.mesh)
self.set_object_location(obj=bpy_obj, location=part.location)
car_collection.objects.link(bpy_obj)
for shape_key in part.mesh.shape_keys:
self.make_shape_key(obj=bpy_obj, shape_key=shape_key)
class TrackImporter(bpy.types.Operator):
"""Import NFS4 Track Operator"""
bl_idname = "import_scene.nfs4trk"
bl_label = "Import NFS4 Track"
bl_description = "Import NFS4 track files"
bl_options = {"REGISTER", "UNDO"}
bpy.types.Scene.nfs4trk = None # type: ignore[attr-defined]
directory: StringProperty( # type: ignore[valid-type]
name="Directory Path",
description="Directory containing the track files",
maxlen=1024,
default="",
)
mode: EnumProperty( # type: ignore[valid-type]
name="Mode",
items=(
(
"GLTF",
"GLTF target",
"Parametrized import of visible track geometry, lights, animations, "
"collision geometry and more. Stores data that can't be represented in "
"GLTF 'extras' fields.",
),
(
"BLENDER",
"Blender target",
"This option should be used when accurate look in Blender is desired. "
"Some data, such as vertex shading, can't be viewed in Blender without specific "
"shader node connections. Such connections are on the other hand poorly understood "
"by exporters, such as the GLTF exporter. Therefore this mode must never be "
"used if you intent to export the track to GLTF. Vertex shading is always enabled "
"in this mode.",
),
),
description="Select importer mode",
)
night: BoolProperty( # type: ignore[valid-type]
name="Night on", description="Import night track variant", default=False
)
weather: BoolProperty( # type: ignore[valid-type]
name="Weather on", description="Import rainy track variant", default=False
)
mirrored: BoolProperty( # type: ignore[valid-type]
name="Mirrored on", description="Import mirrored track variant", default=False
)
import_shading: BoolProperty( # type: ignore[valid-type]
name="Import vertex shading",
description="Import original vertex shading to obtain the 'original' track look",
default=False,
)
import_collision: BoolProperty( # type: ignore[valid-type]
name="Import collision (experimental)",
description="Import collision meshes (ending with -colonly)",
default=False,
)
import_actions: BoolProperty( # type: ignore[valid-type]
name="Import animation actions (experimental)",
description="Import track animation actions from CAN files, such as object destruction animation",
default=False,
)
import_cameras: BoolProperty( # type: ignore[valid-type]
name="Import cameras (experimental)",
description="Import track-specific replay cameras",
default=False,
)
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[int] | set[str]:
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context: bpy.types.Context) -> set[int] | set[str]:
directory = Path(self.directory)
# This should get us from track directory to game root directory
game_root = directory.parent.parent.parent
track = TrackData(
directory=Path(self.directory),
game_root=game_root,
mirrored=self.mirrored,
night=self.night,
weather=self.weather,
)
import_shading = self.import_shading
import_strategy: TrackImportStrategy
if self.mode == "GLTF":
import_strategy = TrackImportGLTF(material_map=track.get_polygon_material)
elif self.mode == "BLENDER":
import_strategy = TrackImportBlender(material_map=track.get_polygon_material)
import_shading = True
else:
return {"CANCELLED"}
import_strategy.import_track(
track=track,
import_collision=self.import_collision,
import_shading=import_shading,
import_actions=self.import_actions,
import_cameras=self.import_cameras,
)
return {"FINISHED"}
class CarImporter(bpy.types.Operator):
"""Import NFS4 Car Operator"""
bl_idname = "import_scene.nfs4car"
bl_label = "Import NFS4 Car"
bl_description = "Import NFS4 Car files"
bl_options = {"REGISTER", "UNDO"}
bpy.types.Scene.nfs4car = None # type: ignore
directory: StringProperty( # type: ignore
name="Directory Path",
description="Directory containing the car files",
maxlen=1024,
default="",
)
import_interior: BoolProperty( # type: ignore[valid-type]
name="Import interior", description="Import car interior geometry", default=False
)
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[int] | set[str]:
wm = context.window_manager
wm.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context: bpy.types.Context) -> set[int] | set[str]:
car = VivData.from_file(Path(self.directory, "CAR.VIV"))
logger.debug(car)
if self.import_interior:
resource = one(car.interior_materials)
parts = car.interior
else:
resource = one(car.body_materials)
parts = car.parts
importer = CarImporterSimple(material_map=lambda _: resource)
importer.import_car(parts)
return {"FINISHED"}
def menu_func(self: Any, context: bpy.types.Context) -> None:
self.layout.operator(TrackImporter.bl_idname, text="Track resources")
self.layout.operator(CarImporter.bl_idname, text="Car resources")
def register() -> None:
bpy.utils.register_class(TrackImporter)
bpy.utils.register_class(CarImporter)
bpy.types.TOPBAR_MT_file_import.append(menu_func)
def unregister() -> None:
bpy.utils.unregister_class(TrackImporter)
bpy.utils.unregister_class(CarImporter)
bpy.types.TOPBAR_MT_file_import.remove(menu_func)
if __name__ == "__main__":
register()
Once this file / script is loaded you can hit the play button in the scripting window. Bear in mind that the project’s save location will be used from the importer to storage the imported images and assets in subfolder(s) created within the save location.
Now, when you executed the script, you will find the option “Track Resources” under the file > import option.
In the import window you now can browse to your NFS4/NFSHS track-folder within your NFS installation directory.
Warning! Opening Tracks outside the NFS-installation directory will not work as the importer requires global NFS-assets such as sounds, textures and other information.
▶️▶️In case you want to import an Addon-Track, read here.
When you arrived at the track folder, open up the folder containing the track you want to import, e.g. EMPIRE (= Empire City) and click on import.
Now just wait a couple of seconds and keep an eye in the scene overview on the top right corner. Soon you will notice that there will be a couple of folders and objects appearing – that is your imported track!
How to import an NFS-Addon track to blender?
So, it seems that you are interested in opening/importing an community made track in blender. The import itself is working the exact way it does as for official tracks but requires some work right before you can import them.
Setting up the track folder
You – basically – need to to the same kind of work that you would need to do to get the track working in the NFS-game, the major difference here is that you do not need to overwrite the track the addon is based on.
Let’s say you downloaded this track here:
Mololithic Studios for NFS4 High Stakes
Author of Track Conversion: Ryan Trevisol Author of Track Enhancement: KillRide and UnBtable and changes by JimDiabolo & Benyy
It is a replacement for the official NFS-Track “Empire City”. So head over to your track-folder in the NFS4-installation directory, locate the original track folder (in this case “EMPIRE”) and duplicate and rename it as you wish, e.g “MONOLITHIC“.
Now copy all addon-content over the original content within the new folder “MONOLITHIC” – and select overwrite-yes of course.
*any: you can also convert NFS:HS addon-tracks, such as "Lake Diablo" (get it here), but you need to copy the addon-track files over the original NFS:HS track it is replacing, just as you would do if you want to play the addon track ingame as the tools are requiring all animation, sounds etc. - read the tutorial for addon-tracks here.
Bring the Thrill of Classic Racing Games Back to Life!
Are you a fan of the legendary “Need for Speed” series? Imagine being part of a project that brings that heart-pounding excitement into the modern era with Unreal Engine 5.
I’m Timo, a solo developer working passionately on “High Stakes,” a tribute to the golden age of racing games. But I need your help to make it happen! I’m raising funds to purchase key Unreal Engine Marketplace extensions that will save months of development time and bring authentic police chases and AI opponents into the game.
Join the Race!
Your support can bring these features to life. Any contribution helps, no matter the size. Let’s recreate the magic of those classic games together!